소스 코드를 완벽하게 파악하고 있는 Accelerate Solutions 팀은 Unity 엔진을 최대한 활용할 수 있도록 수많은 고객을 지원합니다. 팀은 크리에이터 프로젝트를 심도 있게 분석하여 속도, 안정성, 효율성 등을 향상시키기 위해 최적화할 부분을 파악합니다. Unity에서 가장 경력이 많은 소프트웨어 엔지니어로 이루어진 이 팀과 함께 모바일 게임 최적화에 관한 전문적인 지식을 공유하는 자리를 마련했습니다.
모바일 게임 최적화에 관한 인사이트를 공유하기 시작하면서, 원래 계획한 하나의 블로그 포스팅에 담기에는 너무나 방대한 정보가 있다는 사실을 알게 되었습니다. 따라서 이 방대한 지식을 한 권의 전자책(여기에서 다운로드 가능)과 75가지 이상의 실용적인 팁을 담은 블로그 포스팅 시리즈를 통해 제공하기로 했습니다.
이번 시리즈의 첫 게시물에서는 프로파일링, 메모리, 코드 아키텍처를 통해 게임 성능을 개선하는 방법을 자세히 들여다 봅니다. 이후 몇 주 이내에 두 개의 포스팅이 추가로 업로드될 예정입니다. 첫 번째는 UI 물리에 대해, 두 번째는 오디오와 에셋, 프로젝트 설정, 그래픽스에 대해 다룹니다.
시리즈 전체 내용을 지금 바로 확인하고 싶다면 무료로 전자책을 다운로드하세요.
그럼 시작하겠습니다.
모바일 성능 데이터의 프로파일링과 수집 및 활용 과정은 진정한 모바일 성능 최적화가 시작되는 지점입니다.
개발 초기부터 타겟 기기에서 자주 프로파일링 실행하기
Unity 프로파일러는 사용 시 애플리케이션에 대한 필수 성능 정보를 제공합니다. 출시가 멀지 않은 시점이 아닌 개발 초기에 프로젝트를 프로파일링하세요. 여러 오류나 성능 문제를 발생 즉시 조사하세요. 프로젝트의 '성능 시그니처'를 개발하면서 새로운 문제를 보다 쉽게 발견할 수 있습니다.
에디터에서 프로파일링을 수행하면 다양한 시스템의 상대적인 게임 성능에 관한 정보를 얻을 수 있는 반면, 각 기기를 대상으로 프로파일링하면 보다 정확한 인사이트를 확보할 수 있습니다. 가능하면 타겟 기기에서 개발 빌드를 프로파일링하세요. 지원할 기기 중 최고 사양의 기기와 최저 사양의 기기 모두를 프로파일링하고 모두 최적화해야 합니다.
Unity 프로파일러와 더불어 다음과 같은 iOS 및 Android의 네이티브 툴을 사용하면 각 엔진에서 추가로 성능 테스트를 수행할 수 있습니다.
- iOS: Xcode 및 Instruments
- Android: Android Studio 및 Android Profiler
특정 하드웨어는 Arm Mobile Studio, Intel VTune, 및 Snapdragon Profiler 등 추가 프로파일링 툴을 활용할 수 있습니다. 자세한 내용은 Profiling Applications Made with Unity 학습 자료를 참고하세요.
올바른 영역 최적화하기
게임 성능을 저하시키는 요인을 추정하거나 가정하지 않도록 합니다. Unity 프로파일러 및 플랫폼별 툴을 사용하여 성능 저하의 정확한 원인을 찾으세요.
물론 여기에서 설명하는 최적화가 모두 애플리케이션에 적용되지는 않습니다. 다른 프로젝트에는 적합했던 최적화라도 현재 프로젝트에는 적용되지 않을 수 있습니다. 실제 병목 지점을 파악하고 실질적으로 최적화가 필요한 부분에 집중하세요.
Unity 프로파일러의 작동 방식 이해하기
Unity 프로파일러는 런타임 시 성능 저하 또는 중단의 원인을 감지하고 특정 프레임 또는 시점에 발생하는 상황을 보다 정확하게 이해하는 데 도움이 될 수 있습니다. CPU 및 메모리 트랙을 기본적으로 활성화하세요. 물리를 많이 사용하는 게임이나 음악에 기반한 게임플레이를 개발 중이라면 필요에 따라 렌더러, 오디오, 물리와 같은 보조 프로파일러 모듈을 모니터링할 수 있습니다.
Development Build 및 Autoconnect Profiler를 선택하여 기기에 애플리케이션을 빌드하거나 수동으로 연결하여 앱 시작 시간을 단축하세요.
프로파일링을 실행할 타겟 플랫폼을 선택하세요. 녹화 버튼은 애플리케이션 재생을 몇 초 동안 추적합니다(기본 300 프레임). 더 오래 캡처해야 한다면 Unity > Preferences > Analysis > Profiler > Frame Count로 이동하여 이 값을 2000까지 늘릴 수 있습니다. 이렇게 하면 Unity 에디터가 더 많은 CPU 작업을 수행하고 더 많은 메모리를 사용하지만, 상황에 따라 유용할 수 있습니다.
이는 계측 기반 프로파일러로 ProfileMarkers에 명시적으로 래핑된 코드 타이밍을 프로파일링합니다(예: MonoBehaviour의 Start나 Update 메서드 또는 특정 API 호출). 또한 Deep Profiling 설정을 사용하면 Unity는 스크립트 코드에 있는 모든 함수 호출의 시작과 끝을 프로파일링하여 정확히 애플리케이션의 어느 부분에서 성능 저하가 발생하는지 제시합니다.
게임을 프로파일링할 때는 성능 불안정과 평균 프레임의 비용을 모두 살펴보는 것이 좋습니다. 각 프레임에서 발생하는 높은 비용의 작업을 파악하고 최적화하면 타겟 프레임 속도 이하에서 실행되는 애플리케이션에 더 유용할 수 있습니다. 스파이크 현상을 살펴볼 때는 물리, AI, 애니메이션 등의 고비용 작업을 먼저 검토한 다음 가비지 컬렉션을 살펴봅니다.
창을 클릭하여 특정 프레임을 분석하세요. 그리고 나서 타임라인 또는 계층 구조 뷰를 사용합니다.
- 타임라인: 특정 프레임의 타이밍을 시각적으로 요약하여 제시합니다. 이를 통해 각 활동이 다양한 스레드 전반에서 서로 어떤 관계를 맺고 있는지 시각화할 수 있습니다. 이 옵션을 사용하여 CPU 바운드 또는 GPU 바운드 여부를 판단하세요.
- 계층 구조: 그룹화된 ProfileMarkers의 계층 구조를 제시합니다. 이를 통해 밀리초 단위(Time ms 및 Self ms)의 시간 비용을 기준으로 샘플을 정렬할 수 있고, 함수에 대한 호출 수와 프레임의 관리되는 힙 메모리(GC Alloc)의 양도 알 수 있습니다.
여기에서 Unity 프로파일러의 전체 개요를 읽어보세요. 프로파일링이 생소하다면 Unity 프로파일링 소개 영상을 시청해 보세요.
프로젝트에서 최적화를 수행하기 전에 먼저 Profiler .data 파일을 저장하세요. 변경 사항을 구현하고 수정 이전 및 이후에 저장된 .data 파일을 비교해 보세요. 반복적인 프로파일링, 최적화, 비교를 통해 성능을 향상할 수 있습니다. 이후에도 이 과정을 반복합니다.
Profile Analyzer 사용하기
이 툴을 사용하면 Profiler 데이터의 여러 프레임을 집계한 다음 관심 있는 프레임을 찾을 수 있습니다. 프로젝트를 변경하면 Profiler에 어떤 영향을 주는지 확인하고 싶으신가요? Compare 뷰를 사용하면 두 데이터 세트를 로드해서 차이점을 확인할 수 있으므로 변경 사항을 테스트하고 결과를 개선할 수 있습니다. Profile Analyzer는 Unity의 패키지 관리자를 통해 사용할 수 있습니다.
프레임당 정해진 시간 예산으로 작업하기
각 프레임은 목표로 하는 초당 프레임 수(fps)를 기반으로 시간 예산을 가집니다. 30fps로 실행되는 애플리케이션의 경우 프레임당 약 33.33ms(1000ms/30fps)를 허용하는 것이 이상적입니다. 마찬가지로 60fps의 경우 목표 시간 예산은 프레임당 16.66ms입니다.
컷씬이나 로딩 시퀀스 같은 짧은 시간 동안 기기는 예산을 초과할 수도 있지만, 긴 시간 동안 초과할 수는 없습니다.
기기 온도 고려하기
모바일 기기에서는 기기가 과열되거나 OS가 CPU 및 GPU를 서멀 스로틀링할 수 있으므로 최대 시간을 지속적으로 사용하지 않는 것이 좋습니다. 프레임 간 쿨다운을 위해 가용 시간의 약 65%만 사용할 것을 권장합니다. 일반적인 프레임 예산은 30fps에서는 프레임당 약 22ms, 60fps에서는 11ms입니다.
대부분의 모바일 기기에는 데스크톱과 달리 냉각 장치가 없습니다. 물리적인 열기가 성능에 직접적인 영향을 미칠 수 있습니다.
기기가 가열되면 장기적인 문제로 이어지지 않는다 해도 Profiler에서 성능 저하를 인식하고 이를 보고할 수 있습니다. 프로파일링 과열을 방지하기 위해 짧은 간격으로 프로파일링하세요. 이렇게 하면 기기가 냉각되고 실제 사용 조건에서 시뮬레이션됩니다. 일반적으로 다시 프로파일링하기 전에 10~15분 간 기기를 냉각하는 것이 바람직합니다.
GPU 바운드 또는 CPU 바운드 여부 판단하기
Profiler를 통해 CPU가 프레임 예산을 할당된 양보다 오래 사용 중이거나 GPU에 문제가 있는지 파악할 수 있습니다. 다음과 같이 Gfx가 접두어인 마커를 방출하는 방식을 사용합니다.
- Gfx.WaitForCommands 마커가 표시되면 렌더 스레드가 준비되었으나 메인 스레드에 병목 현상이 일어날 가능성을 의미합니다.
- Gfx.WaitForPresent가 빈번하게 발생하면 메인 스레드는 준비되었으나 GPU가 프레임을 표시하기를 기다리는 중이었음을 의미합니다.
Unity는 사용자 생성 코드 및 스크립트에 자동 메모리 관리를 사용합니다. 값이 입력된 로컬 변수처럼 작은 데이터는 스택에 할당됩니다. 더 큰 데이터와 장기 스토리지는 관리되는 힙에 할당됩니다.
가비지 컬렉터는 사용되지 않은 힙 메모리를 주기적으로 파악하여 할당을 해제합니다. 이 작업은 자동으로 실행되지만, 힙의 모든 개체를 검사하는 과정에서 게임이 끊기거나 느리게 실행될 수 있습니다.
메모리 사용량을 최적화하려면 힙 메모리의 할당 및 할당 해제 시점뿐 아니라 가비지 컬렉션의 영향을 최소화하는 방법을 알아야 합니다. 자세한 내용은 관리되는 힙의 이해를 참조하세요.
Memory Profiler 사용하기
Memory Profiler는 별도 애드온(패키지 관리자에서 실험 패키지 또는 프리뷰 패키지로 제공)으로 관리되는 힙 메모리의 스냅샷을 캡처하여 단편화 및 메모리 누출 같은 문제를 식별할 수 있습니다.
메모리에 보관된 네이티브 오브젝트에 대한 변수를 추적하려면 Tree Map 뷰를 클릭하세요. 여기에서는 과도하게 큰 텍스처 또는 중복 에셋과 같은 일반적인 메모리 사용량 문제를 확인할 수 있습니다.
Unity의 Memory Profiler를 활용하여 메모리 사용량을 개선하는 방법을 알아보세요. 공식 Memory Profiler 기술 자료도 확인할 수 있습니다.
가비지 컬렉션의 영향 줄이기
Unity는 Boehm-Demers-Weiser 가비지 컬렉터를 사용하며, 이 컬렉터는 프로그램 코드의 실행을 중단하고 작업이 완료될 때만 일반 실행을 재개합니다.
힙의 불필요한 할당은 GC(가비지 컬렉션) 스파이크를 유발할 수 있으므로 유의해야 합니다.
- 문자열: C#에서 문자열은 값 유형이 아닌 참조 유형입니다. 불필요한 문자열의 생성 또는 조작을 줄이세요. JSON, XML과 같은 문자열 기반 데이터 파일은 구문 분석하지 않는 것이 좋습니다. 데이터를 ScriptableObjects 또는 MessagePack이나 Protobuf 같은 형식으로 대신 저장하세요. 런타임에서 문자열을 빌드해야 하는 경우 StringBuilder 클래스를 사용합니다.
- Unity 함수 호출: 일부 함수는 힙 할당을 생성합니다. 참조를 루프 도중에 할당하지 말고 배열에 저장하세요. 아울러 가비지 생성을 방지하는 함수들을 활용하세요. 예를 들어 문자열을 GameObject.tag와 직접 비교하는 대신 GameObject.CompareTag를 사용하는 방법이 있습니다(새로운 문자열 반환은 가비지를 생성합니다).
- 박싱: 참조 유형 변수 대신 값 유형 변수를 전달하지 않도록 합니다. 이렇게 하면 임시 오브젝트가 생성되며 동반되는 잠재적 가비지가 값 유형을 암묵적으로 타입 오브젝트로 변환합니다(예: int i = 123, object o = i). 대신 전달하려는 값 유형에 구체적인 오버라이드를 제공해 보세요. 이러한 오버라이드에는 제네릭이 사용될 수도 있습니다.
- 코루틴: yield는 가비지를 생성하지 않지만 새로운 WaitForSeconds 오브젝트를 만들면 가비지가 생성됩니다. yield 라인에서 생성하는 대신 WaitForSeconds 오브젝트를 캐시하고 재사용하세요.
- LINQ 및 정규식: 두 가지 모두 박싱에서 가비지를 생성합니다. 성능이 문제라면 LINQ와 정규식을 사용하지 마세요. 새로운 배열을 만드는 대신 for 루프와 리스트를 사용하세요.
가능한 경우 가비지 컬렉션 측정하기
가비지 컬렉션 멈춤 현상이 게임의 특정 지점에 영향을 주지 않는다면 System.GC.Collect로 가비지 컬렉션을 트리거할 수 있습니다.
자동 메모리 관리의 이해에서 이 방식을 활용하는 방법을 참고하세요.
점진적 가비지 컬렉터를 활용하여 GC 워크로드 분할하기
점진적 가비지 컬렉션을 사용하면 프로그램 실행 중에 한 번 길게 중단되는 것이 아니라 훨씬 짧은 중단이 여러 프레임에 걸쳐 여러 번 나타납니다. 가비지 컬렉션이 성능에 영향을 미친다면 이 옵션을 사용하여 GC 스파이크를 줄일 수 있는지 확인해 보세요. Profile Analyzer를 사용하여 이 방식이 애플리케이션에 어떤 이점으로 작용하는지 확인하세요.
Unity PlayerLoop에는 게임 엔진의 코어와 상호작용하기 위한 함수가 포함되어 있습니다. 이 구조는 초기화와 프레임별 업데이트를 처리하는 다양한 시스템을 포함합니다. 모든 스크립트가 이 PlayerLoop를 활용하여 게임플레이를 생성하게 됩니다.
프로파일링 시에는 PlayerLoop 아래에 프로젝트의 사용자 코드가 표시됩니다(EditorLoop 아래에는 에디터 컴포넌트).
PlayerLoop 및 스크립트의 수명 주기를 파악하세요.
다음과 같은 유용한 팁으로 스크립트를 최적화할 수 있습니다.
Unity PlayerLoop 이해하기
Unity 프레임 루프의 실행 순서를 이해해야 합니다. 모든 Unity 스크립트는 사전에 정해진 순서대로 여러 이벤트 함수를 실행합니다. Awake, Start, Update 및 스크립트의 수명 주기를 생성하는 다른 함수들 사이의 차이점을 이해해야 합니다.
이벤트 함수의 구체적인 실행 순서는 스크립트 수명 주기 플로우 차트를 참고하세요.
매 프레임에 실행되는 코드 최소화하기
코드를 반드시 모든 프레임에 실행해야 하는지 확인하세요. 불필요한 로직을 Update, LateUpdate, FixedUpdate에서 제외하세요. 이러한 이벤트 함수에는 프레임마다 업데이트해야 하는 코드를 편리하게 배치할 수 있으며, 같은 빈도로 업데이트할 필요가 없는 로직은 추출됩니다. 가능하다면 상황이 바뀌는 경우에만 로직을 실행하세요.
반드시 Update를 사용해야 한다면 n개 프레임마다 코드를 실행하는 방안을 검토해 보세요. 이는 여러 프레임에 대규모 워크로드를 분산하는 일반적인 기법인 타임 슬라이싱의 적용 방식 중 하나이기도 합니다. 이 예에서는 3개 프레임마다 한 번씩 ExampleExpensiveFunction을 실행합니다.
Start/Awake에서 대규모 로직 사용 방지
첫 번째 씬을 로드하면 다음과 같은 함수가 각 오브젝트에 대해 호출됩니다.
- Awake
- OnEnable
- Start
애플리케이션이 첫 번째 프레임을 렌더링하기 전까지는 이러한 함수에서 고비용 로직의 사용을 피하세요. 그렇게 하지 않으면 필요 이상으로 로딩 시간이 길어질 수 있습니다.
첫 번째 씬 로드에 대한 자세한 내용은 이벤트 함수의 실행 순서를 참조하세요.
빈 Unity 이벤트 방지
빈 MonoBehaviours도 리소스를 필요로 하므로 비어 있는 Update 또는 LateUpdate 메서드는 제거해야 합니다.
테스트에 이러한 메서드를 사용하고 있다면 다음과 같은 프리 프로세서 지시문을 사용하세요.
빌드에 불필요한 오버헤드가 유입되는 일 없이 에디터 내에서 Update를 자유롭게 사용하여 테스트할 수 있습니다.
Debug Log 구문 제거하기
Log 구문, 특히 Update, LateUpdate 또는 FixedUpdate에 있는 Log 구문은 성능을 낮출 수 있습니다. 빌드를 만들기 전에 Log 구문을 비활성화하세요.
이 작업을 보다 쉽게 하려면 프리 프로세서 지시문과 함께 조건부 속성을 만드는 것이 좋습니다. 예를 들어 다음과 같은 커스텀 클래스를 만듭니다.
커스텀 클래스로 로그 메시지를 생성합니다. Player Settings에서 ENABLE_LOG 프리 프로세서를 비활성화하면 모든 Log 구문이 동시에 사라집니다.
문자열 파라미터 대신 해시 값 사용하기
Unity는 내부적으로 애니메이터나 머티리얼, 셰이더 프로퍼티를 식별할 때 문자열 이름을 사용하지 않습니다. 빠른 처리를 위해 모든 프로퍼티 이름이 프로퍼티 ID에 해시되어 있으며, 해당 ID가 실제로 프로퍼티를 식별하는 데 사용됩니다.
애니메이터, 머티리얼 또는 셰이더에서 Set 또는 Get 메서드를 사용하는 경우에는 문자열 값 메서드 대신 정수 값 메서드를 사용하세요. 문자열 메서드 역시 문자열 해싱을 수행한 다음 해시된 ID를 정수 값 메서드로 전달하기 때문입니다.
애니메이터 프로퍼티 이름에는 Animator.StringToHash를, 머티리얼 및 셰이더 프로퍼티 이름에는 Shader.PropertyToID를 사용하세요.
올바른 데이터 구조 선택
한 번 선택한 데이터 구조는 프레임당 수천 번씩 반복되므로 효율성에 영향을 줍니다. 컬렉션에 리스트, 배열 또는 딕셔너리 중 무엇을 사용해야 할지 고민하고 계신가요? 올바른 자료 구조를 선택하기 위한 일반적인 가이드인 C#의 데이터 구조 MSDN 가이드를 참고하세요.
런타임 시 컴포넌트 추가 방지
런타임에 AddComponent를 호출하면 비용이 발생합니다. Unity는 런타임에 컴포넌트가 추가될 때마다 중복 또는 기타 필요한 컴포넌트를 확인해야 합니다.
이미 설정된 원하는 컴포넌트로 프리팹을 인스턴트화하는 것이 일반적으로 더 효과적입니다.
게임 오브젝트 및 컴포넌트 캐시하기
GameObject.Find, GameObject.GetComponent, Camera.main(2020.2 이전 버전)은 비용이 많이 들 수 있으므로 Update 메서드에서는 호출하지 않는 것이 가장 좋습니다. 대신 Start에서 호출하고 결과를 캐시하세요.
다음 예는 반복적인 GetComponent 호출의 비효율성을 보여줍니다.
함수의 결과가 캐시되므로 GetComponent는 한 번만 호출하세요. Update에서 GetComponent를 추가로 호출하지 않아도 캐시된 결과를 재사용할 수 있습니다.
오브젝트 풀 사용하기
Instantiate 함수 및 Destroy 함수는 가비지와 가비지 컬렉션 스파이크를 야기하며, 일반적으로 속도가 느린 프로세스입니다. 게임 오브젝트를 계속 인스턴트화하고 삭제하기보다는(예: 총알 발사) 재사용 및 재활용할 수 있는 사전에 할당된 오브젝트 풀을 사용해 보세요.
CPU 스파이크가 적을 때 게임 내 한 지점(예: 메뉴 화면)에서 재사용 가능한 인스턴스를 생성합니다. 컬렉션으로 이러한 오브젝트 '풀'을 추적합니다. 게임플레이 중에는 필요할 때 다음으로 사용 가능한 인스턴스를 사용 설정하고, 오브젝트를 삭제하는 대신 사용 중지한 다음 풀로 돌려 보내면 됩니다.
이렇게 하면 프로젝트에서 관리되는 할당의 수가 줄어들기 때문에 가비지 컬렉션 문제를 방지할 수 있습니다.
Unity에서 간단한 오브젝트 풀링 시스템을 만드는 방법은 여기에서 알아보세요.
스크립터블 오브젝트 사용하기
변하지 않는 값 또는 설정은 MonoBehaviour가 아닌 스크립터블 오브젝트에 저장하세요. 스크립터블 오브젝트는 한 번만 설정하면 되는 프로젝트 내부의 에셋으로 게임 오브젝트에 직접 연결할 수 없습니다.
스크립터블 오브젝트에서 필드를 생성하여 값 또는 설정을 저장한 다음 MonoBehaviours에서 스크립터블 오브젝트를 참조하세요.
스크립터블 오브젝트의 필드를 사용하면 MonoBehaviour로 오브젝트를 인스턴스화할 때마다 데이터의 불필요한 중복을 방지할 수 있습니다.
스크립터블 오브젝트 소개 튜토리얼에서 스크립터블 오브젝트의 장점을 알아보세요. 관련 기술 자료는 여기에서도 찾을 수 있습니다.
'게임엔진(GameEngine) > Unity3D' 카테고리의 다른 글
렌더링 순서 - Queue 태그 (0) | 2022.12.24 |
---|---|
효과적인 C# 메모리 관리 기법 (0) | 2022.12.04 |
C#은 stackless 코루틴 (0) | 2022.12.04 |
Unity vertex / fragment Shader01 -빨강 쉐이더 (0) | 2022.12.02 |
Keyword Boolean (0) | 2022.11.27 |