2025
05.13

 

[Unite Seoul 2025] Unity 프로젝트 개발 시 반드시 체크해야 할 최적화 관련 기능 공유

https://youtu.be/oj87uQp5waA?si=u1-AO2HhuEqEGpmD

 

1. 쉐이더의 높은 메모리 점유

1-1) Shader Variant

- 쉐이더에 키워드가 추가될 때 마다 2의 지수형태로 증가한다.

- 키워드가 켜진 쉐이더, 안켜진 쉐이더를 각각 경우의 수로 빌드를 해야되니 배리언트가 증가하는 것

- 특히 에셋스토어에서 구매한 쉐이더의 경우 주의할 필요가 있다.

 

1-2) Dynamic Shader loading

이를 위해 Dynamic Shader loading 이라는 기능이 있다.

- Chunk 단위 variants 압축/해제한다.

- Player Settings > Shader Variant Loading Settings

- https://unity.com/kr/blog/engine-platform/2021-lts-improvements-to-shader-build-times-and-memory-usage

 

- 기본값은 청크갯수가 0으로 설정(비활성화)되어 있다.

- 청크 갯수를 늘리고 청크 사이즈는 작은 값부터 시작해서 테스트 해보길 권장한다.

 

1-3) Shader Variants Stripping

- Editor Option에서 Project Settings > Graphics 에서 Shader Stripping 설정이 있으니 게임 내에서 사용하는 피쳐를 제한하여 배리언츠를 줄이도록 하자.

- #pragma 지시문을 점검하자. multi_compile 로 지정된 것이 쉐이더 배리언츠를 늘리는 주 원인이다.

 

- 모든 것을 다 했는데도 배리언츠의 사용량이 높다면 직접 스트리핑을 하는 것이 중요해야한다.

- Play log 에서 실제 사용하는 variant의 정보를 추출하고 IPreprocessShaders 를 구현하자

- https://unity.com/blog/engine-platform/shader-variants-optimization-troubleshooting-tips

 

1-4) Unity Subsystems

- PersistentManager.Remapper

    Instance ID와 File GUID, Local ID Mapping 정보

- SerializedFile

    에셋 번들 메타 데이터

    https://unity.com/kr/blog/technology/tales-from-the-optimization-trenches-saving-memory-with-addressables

 

- 둘 다 에셋 번들 때문에 커지는 문제가 있다. 장면에 필요한 모든 에셋을 포함한 번들을 만드는 것은 현실적으로 불가능하다.

- 최소한으로 필요한 번들들을 로드해야한다.

- AssetBundle.Unload(false); 로 에셋 번들을 내리면 관련된 인스턴스가 같이 내려가기 때문에 Custom AssetBundle Provider 를 만들어서 제어하는 것도 가능하다.

 

1-5) IL2CPP metadata

- 메모리 프로파일러에서 Virtual Machine 라는 이름의 영역으로 확인할 수 있다.

- 일부 고정 크기, 일부 실행 중 크기가 증가한다.

- 전체 크기를 알기위해서는 빌드 옵션에서 Platform Settings 에 Script Debugging 옵션을 켜주게 되면 프로파일러에서 il2cpp 필터링을 켜서 확인할 수 있다. (물론 디버그 정보를 포함해서 실제보다 더 커진다) 

 

이를 최적화하기 위해서 3가지 고려사항이 있다.

 

- Player옵션의 Optimization > Managed Stripping Level > Medium 으로 설정하는 것

  많은 개발사들이 이 옵션을 올렸다가 실제 사용하는 코드가 삭제되버려서 사용하지 않는 경향이 있다.

  link.xml 을 사용하여 네임스페이스를 추가할 수 있으니 이를 설정하면 Medium 이상을 사용하면 된다. 

  High랑 Medium의 차이가 20% 내외라서 Medium을 추천한다.

- Player옵션의 IL2CPP Code Generation - Faster (Smaller) Builds 을 사용하면 제네릭 코드의 양을 줄일 수 있다. 

- Pacakage Manager에서 불필요한 패키지와 플러그인 제거하자.

  모듈의 사이즈는 Library/Bee/artifacts/Platform/ManagedStripped 에서 확인하고 지우는 것을 고려하자.

 

1-6) Memory Fragmentation

- 잦은 작은 할당에 의해 메모리 단편화가 발생한다.

- 필요한 최대 메모리를 미리 할당하여 재사용하고 임시할당은 최소화 하는게 현실적인 대안이다.

 

- 텍스트 데이터를 파싱할 때 단편화가 급격하게 일어나는 현상이 많으니 주의하길.

- Object.name 같은 복사본 반환 멤버 주의

 

2. CPU Stuttering

2-1) Unity Profiler - CPU module

- 유니티 프로파일러는 태그를 수집하는 방식

- Depp profiler 를 쓰지 않더라도 call stack 옵션을 켜서 확인 가능하니 참고바람.

 

2-2) Instantiate

- 복잡한 인스턴스를 생성할 때 발생하는 히칭현상

- Serialized field 규모에 비례한 CPU 부하

   생성 시점에 분리됨

- Instantiate도 Object.InstantiateAsync 로 비동기로 처리할 수 있다.

  생성은 Produce(비동기), Copy(비동기), Awake(동기) 3단계로 구분되는데, 앞 두 단계가 deserialize되면서 굉장히 부하가 크다.

 

2-3) GC.Collect

- GC allocation을 최소화해서 GCCollect를 최대한 줄이는 것이 중요하다.

- GarbageCollector.GCMode 를 사용해서 GCCollect 자체를 아예 비활성화 할 수 있지만, 추천하긴 어려움

 

3. 주요 CPU 부하 요인

3-1) 렌더링

- 그릴 때는 SRP Batcher나 Instacing을 사용하는게 중요

- Culling을 통해 그릴 물체를 사전에 제거하는 것도 유효

  - Camera.layerCullDistances,

  - Light.layerShadowCullDistances,

  - LOD culled

  - CullingGroup의 사용 (하지만 그림자문제가 있을 수 있다)

 

3-2) UGUI

- 당연하지만 UI는 동적 요소(Animator, Scroll)와 정적 요소를 분리하자.

- UI에서 Layout System을 사용하는 경우 부하를 Additional Layout Update라는 이름으로 프로파일러에 표시되니 확인하고 최소화하거나 비활성화 하는게 좋다.

- RectMask2D 는 컬링할 때 CPU 부하가 꽤 있는 편. 특히 Text 포함시 급격한 증가


3-3) Animator

- 기본적으로 애니메이터는 잡시스템을 사용해서 효율적으로 동작하게 설계되어있다.

- 하지만 특정 이벤트를 사용하면 메인 쓰레드에서 돌아가도록 강제된다.

- 애니메이터자체로도 부하가 있기 때문에 작은 클립 (애니메이션 커브가 400 이하)만 재생할 꺼라면 애니메이터 보다는 레거시Animation 컴포넌트를 사용하는게 성능적으로 이득이다. 

- 애니메이터가 씬에 많은 경우 한 프레임에 다같이 업데이트하는건 부하가 클 수 있어서 playable을 사용해서 그룹 단위로 묶어서 잡시스템으로 Animator.Update 를 활용해 재생할 수 도 있다. 

 

3-4) Skninning

- 씬에 스킨드메쉬가 많은 경우 부하가 걸릴 수 있다.

   - Quality Settings > Skin weight or SkinnedMeshRenderer > Quality 에서 최대 영향을 주는 본 갯수를 제한 해서 성능을 올릴 수 있다. ( 스킨드메쉬렌더러 컴포넌트에서도 개별 설정 가능 ) 

-  Skinned Mesh Renderer > UpdateWhenOffscreen 이 기본으로 켜져있는데, 비활성화 하면 안보일때 메쉬를 업데이트 하지 않는다.

- Player Settings > GPU Skinning 에서 CPU, GPU, GPU (Batched) 를 설정할 수 있는데, GPU-Batched는 대상이 적을 경우 병합처리가 부하가 걸려 오히려 불리하다 (Static 배칭처럼)

 

3-5) 카메라

- 활성된 Camera를 최소화하자. UICamera 대신 Screen overlay를 사용할 것.

- Camera의 목적에 맞는 SRP asset 지정하자

 

3-6) Multiple FixedUpdate

기본적으로 FixedTimeUpdate는 이전 프레임에서 렉이 발생했을때 현재 프레임에서 보상하기 위해 여러번 발생하게 된다.

따라서 TimeStep을 Update와 동일하게 맞춰주거나, (기본값은 FixedTimeStep이 0.02라서 50프레임 기준이다)

스크립트로 수동호출하는 방식을 고려할 수 있다.

 

프로젝트 세팅 점검하자

- Project Settings > Time > FixedTimeStep

- Project Settings > Physics > Simulation Mode

 

3.7) 레이캐스트

- 레이캐스트는 수집한다음 필터하는 방식으로 구현된다.

- 기본 distance는 무한대로 설정되어있기 때문에 꼭 적절한 값으로 수정하여 사용하기 바란다.

- 레이어마스크를 통해 수집되는 대상을 줄이도록 하자

 

3.8) Renderer의 Bounding Volume 갱신 부하

- 트랜스폼은 잡시스템을 통해 갱신되지만 root Transform 단위로 이루어진다.

- 모든 객체가 하나의 단일 루트를 가지고 있다면 비효율적으로 처리된다.

- 변경이 작은 경우 rootTransform 을 적절하게 분산하자

- 트랜스폼 변경이 필요없는 경우 vertex shader에서 변환하는 편이 부하가 적다. 

 

4. GPU 부하 요인

4-1) Pixel overdraw

- URP Rendering Debugger > Overdraw Mode 를 통해 오버드로우를 디버깅 가능하다

- 파티클 등은 투명한 영역이 생각보다 많은데 투명 영역이 제거된 meshf를 사용하여 최적화하자

- UI의 캔버스 렌더러에는 Canvas.cullTransparentMesh 옵션이 있다. 알파가 0인경우 UI요소를 그리지 않는 것. 최신버전에서는 기본값으로 켜져있는데, 이전버전에서는 꺼져있었기 때문에 이전버전에서 마이그레이션을 한 경우라면 별도로 체크해줘야한다.