프로그래밍/Unity
[알쓸유잡 : 모바일 최적화] 영상 정리하기
Doublsb
2024. 1. 20. 13:27
이번 포스팅은 유니티 공부를 할 때 항상 신세를 지고 있는 알쓸유잡 콘텐츠를 공부하고 기록해보는 글이 되겠다.
특히나 이번 영상은 최적화였기 때문에, 프로파일러 안의 숫자를 하나라도 더 줄이면 쾌감을 느끼는 편인 나는 어쩔 수 없이 글을 쓸 수밖에 없었다. 음... 변태같은 소리는 뒤로 하고 본론으로 들어가자.
소개
https://www.youtube.com/live/-IrpQzZi_p8?si=tz7lyopfIHhbdPyX
https://blog.unity.com/engine-platform/updated-2022-lts-best-practice-guides
이번에 새로 나온 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 & 정규 표현식
- 박싱/언박싱이 많이 일어나므로 자주 호출되는 함수에서는 지양하자
- String
- 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에 넣어야 하므로 메모리 겁나 잡아먹음
- 모바일에는 적합하지 않음
- Forward
- 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를 사용해라
반응형