Unity 검색

Unity의 발전하는 베스트 프랙티스

최근 업데이트: 2018년 12월

이 강연의 내용: 이안 던도어(Ian Dundore)가 제공하는 스크립팅 성능 및 최적화 관련 팁으로, 데이터 중심 설계를 지원하기 위한 Unity 아키텍처의 발전 방향을 논의합니다.

Unity가 개선됨에 따라 기존 방식은 엔진 성능 극대화에 더 이상 적합하지 않을 수 있습니다. 이 포스팅에서는 Unity의 몇 가지 변동사항(Unity 5.x에서 2018.x까지)과 활용 방법에 대한 요약된 내용을 살펴볼 수 있습니다. 이 모든 사례는 이안 던도어(Ian Dundore)의 강연 내용을 바탕으로 하며, 원하신다면 바로 강연 내용을 확인할 수 있습니다.

스크립팅 성능

최적화에서 가장 어려운 작업 중 하나는 핫스팟이 발견된 이후 코드를 최적화하는 방법을 선택하는 것입니다. 여기에는 운영체제의 플랫폼별 세부 사항, CPU, GPU, 스레딩, 메모리 액세스 등 여러 요소가 포함됩니다. 어떤 최적화가 실제로 가장 큰 이익을 창출할 것인지 미리 파악하기란 쉽지 않습니다.

일반적으로 반복 작업이 훨씬 빠른 소규모 테스트 프로젝트에서 최적화를 프로토타이핑하는 것이 가장 좋습니다. 그러나 코드를 테스트 프로젝트에 격리하면 자체적인 문제가 발생하는데, 코드 일부를 격리하면 해당 코드를 실행하는 환경이 변경되기 때문입니다. 즉 스레드 타이밍이 달라질 수 있으며 관리되는 힙이 더 작아지거나 단편화 정도가 감소할 수 있습니다. 따라서 테스트는 신중하게 설계해야 합니다.

먼저 코드에 대한 입력과 입력 변경 시 코드의 대응 방식을 고려하세요.

  • 메모리에 직렬로 배치되어 있고 응집성이 높은(highly-coherent) 데이터에 대해 어떻게 반응하나요?
  • 캐시 응집성이 없는(cache-incoherent) 데이터를 어떻게 처리하나요?
  • 코드가 실행되는 루프에서 코드를 얼마나 제거했나요? 프로세서의 명령 캐시(Instruction Cache) 사용량을 변경했나요?
  • 어느 하드웨어에서 실행되고 있나요? 해당 하드웨어에서 분기 예측(branch prediction)을 얼마나 원활하게 구현하나요? 비순차적인 마이크로 오퍼레이션(Micro Operation)을 얼마나 원활하게 실행하나요? SIMD를 지원하나요?
  • 상당히 멀티스레딩된 시스템(Multi-threaded System)이 있고 시스템의 코어가 비교적 적은 경우와 비교적 많은 경우에 각각 시스템이 어떻게 반응하나요?
  • 코드의 크기 조정 파라미터(Scaling Parameter)가 어떻게 되나요? 인풋(Input) 세트가 커지면서 선형으로 확장되나요, 혹은 선형 이상으로 확장되나요?

실질적으로 테스트 도구로 무엇을 측정하는지를 정확하게 파악해야 합니다.

예를 들어, 두 개의 스트링을 비교하는 단순한 작업을 테스트하는 다음 경우를 고려해 보세요.

C# API가 두 개의 스트링을 비교할 때, 서로 다른 문화권(Culture)에 속한 다른 문자(Character) 간에 일치하도록 로케일 종속 전환(Locale-specific Conversion)을 진행하게 되는데, 이 작업은 매우 느립니다.

C#에서 대부분의 스트링 API는 문화권의 영향을 받지만, String.Equals는 영향을 받지 않습니다.

GitHub에서 String.CS를 열어 String.Equals를 살펴보면, 리플렉션(Reflection) 없이는 직접 호출할 수 없는 프라이빗 함수인 EqualsHelper로 제어를 전달하기 전에 몇 가지 점검을 수행하는 매우 단순한 함수를 볼 수 있습니다.

lp MonoString CS1

EqualsHelper는 간단한 메서드입니다. 이 메서드는 문자열을 한 번에 4바이트씩 살펴보며 입력 문자열의 원시 바이트를 비교합니다. 만약 불일치가 발견되면 중지하고 "false"를 반환합니다.

그러나 다른 방법으로도 문자열이 동일한지 검사할 수 있습니다. 가장 안전한 방법은 비교할 문자열과 StringComparison이라고 하는 열거형의 두 가지 파라미터를 받는 String.Equals를 오버로딩하는 것입니다.

이미 String.Equals의 단일 파라미터 오버로드가 EqualsHelper로 제어를 전달하기 전에 약간의 작업만을 하는 것을 확인했습니다. 두 개 파라미터 오버로드는 무슨 일을 할까요?

두 개 파라미터 오버로드의 코드를 살펴보면, 이는 대규모switch 문을 입력하기 전에 몇 가지 추가 점검을 수행합니다. 이 문은 StringComparison 열거형 인풋 값을 테스트합니다. 단일 파라미터 오버로드와의 패리티(parity)를 찾고 있으므로, 바이트별 비교인 서수 비교(ordinal comparison)가 필요합니다. 이 예시에서는 제어가 4개의 점검을 지나 StringComparison.Ordinal에 도달하게 되는데, 이 때의 코드는 단일 파라미터 String.Equals 오버로드와 매우 유사합니다. 즉, 단일 파라미터 대신 두 개 파라미터 String.Equals 오버로드를 사용한다면 프로세서는 몇 가지 추가적인 비교 작업을 수행하게 됩니다. 이 작업의 진행 속도는 더 느릴 것으로 예상되지만, 테스트해 볼 가치가 있습니다.

스트링이 동일한지 비교하는 모든 방법에 관심이 있다면 String.Equals 테스트에서 멈추어서는 안 됩니다. 서수 비교를 수행할 수 있는 String.Compare 오버로드와, 자체적으로 두 가지 다른 오버로드가 있는 String.CompareOrdinal라는 메서드도 있습니다.

레퍼런스 구현을 위한 간단한 수작업 코딩된 예시를 소개합니다. 이는 두 개 인풋 스트링의 각 문자를 반복적으로 점검하는 단순한 길이 검사용 함수입니다.

이 모든 내용에 대한 코드를 살펴보면, 즉시 유용한 것으로 보이는 4가지 테스트 사례가 있습니다.

  • 최저 성능을 테스트하기 위한 두 개의 동일한 스트링
  • 길이 검사를 우회하기 위한 동일한 길이의 임의의 문자로 구성된 두 개의 스트링
  • String.CompareOrdinal 고유의 독특한 최적화를 우회하기 위한 임의의 문자 및 동일한 길이와 동일한 첫 문자로 이루어진 두 개의 스트링
  • 최상 성능을 테스트하기 위한 임의의 문자 및 서로 다른 길이의 두 개의 스트링

몇 번의 테스트를 통해 String.Equals가 가장 뛰어나다는 것을 알 수 있습니다. 이 결과는 플랫폼, 스크립팅 런타임 버전, Mono 또는 IL2CPP의 사용 여부와 관계없이 동일합니다.

유의사항: String.Equals는 스트링 일치 연산자(==)에서 사용하는 메서드이기 때문에, 코드 전체적으로 a == b를 a.Equals(b)로 변경하지 마세요!

사실, 결과를 봤을 때 직접 코딩한 레퍼런스 구현의 성능이 이상하게도 더욱 떨어집니다. IL2CPP 코드를 살펴보면, 코드가 크로스 컴파일될 때 Unity가 여러 어레이 바운드 검사 및 Null 검사를 추가하는 것을 볼 수 있습니다.

이는 비활성화할 수 있습니다. Unity 설치 폴더에서 IL2CPP 하위 폴더를 찾으세요. IL2CPP 하위 폴더 안에는 IL2CPPSetOptionAttributes.cs가 있습니다. 이를 프로젝트로 드래그하면 Il2CppSetOptionAttribute에 액세스할 수 있습니다.

이 속성을 사용하여 타입(Type) 및 메서드(Method)의 데코레이션이 가능합니다. 자동 Null 검사나 자동 어레이 바운드(Array Bound) 검사를 비활성화하도록 이를 구성할 수 있습니다. 이를 통해 경우에 따라 코드의 속도를 크게 개선할 수 있습니다. 이 특정 테스트 사례에서는 직접 코딩한 스트링 비교 방식의 속도가 약 20% 빨라졌습니다!

lp null 체크 팁

트랜스폼

Unity 에디터 계층(Hierarchy)에서는 확인이 어렵지만 Unity 5와 Unit 2018를 비교했을 때 트랜스폼(Transform) 컴포넌트에 많은 변화가 있습니다. 이러한 변화는 성능 개선에 대한 흥미롭고 새로운 가능성을 제시합니다.

Unity 4와 Unity 5.0에서 트랜스폼을 생성하면 Unity의 기본 메모리 힙(Memory Heap) 어딘가에 오브젝트가 할당되었습니다. 이 오브젝트는 기본 메모리 힙 어느 곳에나 위치할 수 있었습니다. 연속적으로 할당된 두 개의 트랜스폼의 위치가 서로 멀리 떨어질 수도 있었으며, 자식(Child) 트랜스폼과 부모(Parent) 트랜스폼의 거리도 예측할 수 없었습니다.

이로 인해 선형으로 트랜스폼 계층(Transform Hierarchy)에서 반복 작업 시, 선형 반복 작업이 인접한 메모리 영역에서 이루어지지 않았습니다. 이로 인해 L2 캐시 혹은 메인 메모리에서 트랜스폼 데이터를 가져오기를 기다리면서 프로세서가 반복적으로 정지하는 현상이 나타났습니다.

Unity 백엔드(backend)에서 트랜스폼의 포지션(Position), 로테이션(Rotation) 또는 스케일(Scale)에 변화가 있을 시, 해당 트랜스폼은 OnTransformChanged 메시지를 전송했습니다. 이 메시지는 모든 자식 트랜스폼에 전달되어 각자의 데이터를 업데이트하고 트랜스폼 변화에 관련된 다른 컴포넌트에 알리도록 했습니다. 예를 들어 콜라이더가 연결된 자식 트랜스폼은 자식 또는 부모 트랜스폼에 변화가 있을 시 물리 시스템을 업데이트해야 합니다.

이러한 불가피한 메시지는 여러 성능 문제를 야기했는데, 이는 특히 불요(spurious) 메시지를 피할 수 있는 방법이 내장되어 있지 않았기 때문입니다. 자식 트랜스폼과 함께 트랜스폼을 변경할 때 Unity의 OnTransformChanged 메시지를 피할 방법이 없었으며, 이로 인해 많은 CPU 시간이 낭비되었습니다.

이로 인해, 기존 Unity 버전에서 가장 일반적인 조언 중 하나는 트랜스폼 변경 사항을 배치(batch)하는 것이었습니다. 즉, 트랜스폼의 포지션 및 로테이션을 첫 프레임에서 한 번 캡처하고, 프레임 기간 동안 이러한 캐시된 값을 활용 및 업데이트하는 것입니다. 그 후 포지션 및 로테이션에 대한 변경 사항을 프레임의 마지막에 한 번 적용합니다. 이는 Unity 2017.2까지 사용되는 효과적인 방식입니다.

다행히도, Unity 2017.4 및 2018.1부터 OnTransformChanged가 종료되고, TransformChangeDispatch라는 새로운 시스템으로 대체되었습니다.

TransformChangeDispatch는 Unity 5.4에 처음 도입되었습니다. 이 버전에서 트랜스폼은 더 이상 Unity 기본 메모리 힙의 어느 위치에나 할당되는 고립된 오브젝트가 아닙니다. 대신, 씬의 각 루트 트랜스폼은 인접한 데이터 버퍼(Buffer)로 표현되었습니다. TransformHierarchy 구조로 불리는 이 버퍼는 루트 트랜스폼 아래의 모든 트랜스폼에 대한 모든 데이터를 포함하고 있습니다.

뿐만 아니라, TransformHierarchy에는 그 안에 포함된 각 트랜스폼에 대한 메타데이터도 보관됩니다. 이 메타데이터에는 개별 트랜스폼이 "지저분(Dirty)"한지(즉, 해당 트랜스폼이 "깨끗(Clean)"한 것으로 표시된 이후 포지션, 로테이션 또는 스케일이 변경되었는지) 나타내는 비트마스크가 포함됩니다. 또한 Unity의 다른 시스템 중 어느 것이 특정 트랜스폼의 변경과 관련되었는지 추적하기 위한 비트마스크(Bitmask)도 포함되어 있습니다.

이 데이터를 사용해 이제 Unity는 다른 각 내부 시스템에 대해 지저분한 트랜스폼의 목록을 생성할 수 있습니다. 예를 들어, 물리 시스템은 TransformChangeDispatch에 쿼리하여 물리 시스템이 FixedUpdate를 마지막으로 실행한 이후 데이터가 변경된 트랜스폼의 목록을 제공받을 수 있습니다.

하지만 이처럼 변경된 트랜스폼 목록을 작성하기 위해서 TransformChangeDispatch 시스템이 씬의 모든 트랜스폼에 대해 반복 작업을 수행한다면, 트랜스폼이 많은 씬의 경우 작업 속도가 매우 느릴 것입니다. 이는 특히 변경된 트랜스폼의 수가 대부분의 경우 매우 적기 때문입니다.

이를 보완하기 위해 TransformChangeDispatch는 변경된 TransformHierarchy 구조의 목록을 추적합니다. 트랜스폼에 변동이 있을 경우 자신과 자신의 자식을 변경된 것으로 표시하고, TransformChangeDispatch 시스템에 해당 트랜스폼이 저장된 TransformHierarchy를 등록합니다. Unity 내의 다른 시스템에서 변경된 트랜스폼 목록을 요청할 경우, TransformChangeDispatch는 변경된 TransformHierarchy 구조 안에 보관된 각 트랜스폼에 대해 반복 작업을 수행합니다. 해당되는 변동 및 관련 비트 세트가 있는 트랜스폼은 목록에 추가되며 이 목록은 요청을 수행하는 시스템으로 반환됩니다.

이 아키텍처로 인해, 계층을 나눌수록 Unity의 세부적인 변화 추적 기능이 더욱 향상됩니다. 씬의 루트에 트랜스폼이 더 많이 존재할수록, 변동 사항을 찾을 때 점검해야 하는 트랜스폼의 수가 줄어듭니다.

하지만 이는 또 한 가지 시사하는 바가 있습니다. TransformChangeDispatch는 Unity 내부의 멀티스레딩 시스템을 활용해 TransformHierarchy 구조 확인 시 수행해야 하는 작업을 나눕니다. 이처럼 작업을 나누고 결과를 합치는 과정으로 인해 시스템이 TransformChangeDispatch에 변경 목록을 요청할 때마다 약간의 오버헤드가 발생합니다.

대부분의 Unity 내부 시스템은 실행 직전에 프레임당 업데이트를 요청합니다. 예를 들어, 애니메이션 시스템은 씬의 모든 활성화된 애니메이터를 분석하기 직전에 업데이트를 요청합니다. 마찬가지로, 렌더링 시스템은 보이는 오브젝트 목록을 컬링(Culling)하기 전에 씬에서 활성화된 모든 렌더러에 대한 업데이트를 요청합니다.

하지만 물리 시스템의 경우는 약간 다릅니다.

Unity 2017.1와 그 이전 버전에서 물리 업데이트는 동기식으로 진행되었습니다. 콜라이더(Collider)가 연결된 트랜스폼을 이동하거나 회전할 경우 물리 씬을 즉시 업데이트했습니다. 이를 통해 콜라이더의 변경된 포지션 또는 로테이션을 물리 월드에 반영하고 레이캐스트 및 기타 물리 쿼리의 정확도를 보장했습니다.

Unity 2017.2에서는 TransformChangeDispatch를 사용할 수 있도록 물리를 마이그레이션했습니다. 이는 필요한 변화였지만 한편으로는 문제가 발생할 수도 있습니다. 레이캐스트를 진행할 때마다 TransformChangeDispatch로 쿼리하여 변경된 트랜스폼 목록을 받아 물리 월드에 적용해야 했습니다. 이 방식은 트랜스폼 계층의 규모 및 코드가 물리 API를 호출하는 방식에 따라 비용이 많이 들 수 있습니다.

이 행동은 이제 새로운 설정인 Physics.autoSyncTransforms를 통해 관리됩니다. Unity 2017.2부터 Unity 2018.2까지 이 설정의 기본값은 "true"이며, Raycast 또는 Spherecast와 같은 물리 쿼리 API를 호출할 때마다 Unity는 자동으로 물리 월드를 트랜스폼 업데이트에 동기화합니다.

이 설정은 Unity 에디터의 물리 설정 또는 런타임에서 Physics.autoSyncTransforms 프로퍼티(Property)를 설정하여 변경할 수 있습니다. 만약 이를 "false"로 설정하고 자동 물리 동기화를 비활성화하면 물리 시스템은 특정 시점의 변경 사항에 대해서만, 즉 FixedUpdate 실행 직전에만 TransformChangeDispatch 시스템에 쿼리합니다.

물리 쿼리 API 호출 시 성능 문제가 발생할 경우, 이를 해결하기 위한 두 가지 추가적인 방법이 있습니다.

먼저, Physics.autoSyncTransforms을 "false"로 설정하는 방법이 있습니다. 이는 TransformChangeDispatch 및 물리 쿼리를 통해 진행되는 물리 씬 업데이트로 인한 스파이크를 제거합니다.

하지만 이렇게 설정할 경우 콜라이더의 변경 사항은 다음 FixedUpdate까지 물리 씬으로 동기화되지 않습니다. 즉, autoSyncTransforms 비활성화 후 콜라이더를 이동하고, 레이를 콜라이더의 새 위치를 향해 조정 후 레이캐스트를 호출하면, 레이캐스트가 콜라이더를 맞추지 못할 수 있습니다. 이는 레이캐스트가 물리 씬의 최종 업데이트 버전에서 운영 중이지만 물리 씬이 아직 콜라이더의 새로운 위치로 업데이트되지 않았기 때문입니다.

자동 트랜스폼 동기화를 비활성화하면 이와 같이 흔하지 않은 버그가 발생할 수 있기 때문에 문제가 되지 않도록 게임을 주의 깊게 테스트해야 합니다. 물리에서 트랜스폼 변동 사항을 반영하여 물리 씬을 업데이트하도록 하려면 Physics.SyncTransforms를 호출합니다. 이 API는 느리기 때문에 한 프레임에서 여러 번 호출하지 않는 것이 좋습니다!

참고로, Unity 2018.3 이후부터 Physics.autoSyncTransforms의 기본값은 "false"로 설정됩니다.

TransformChangeDispatch 쿼리에 소비되는 시간을 최적화하는 두 번째 방법은 쿼리의 순서를 재배치하고 물리 씬이 새로운 시스템에 더 적합하도록 업데이트하는 것입니다.

기억할 사실은, Physics.autoSyncTransforms가 "true"로 설정되면 모든 물리 쿼리가 TransformChangeDispatch에 변동 사항이 있는지 확인하게 된다는 점입니다. 하지만 TransformChangeDispatch에 확인해야 할 변경된 TransformHierarchy 구조가 없고 물리 시스템이 물리 씬에 적용할 트랜스폼 업데이트가 없다면, 물리 쿼리에 오버헤드가 거의 추가되지 않습니다.

그렇기 때문에 물리 쿼리 전체를 배치로 실행한 후 모든 트랜스폼 변동 사항을 배치로 적용할 수 있습니다. 트랜스폼 변경을 물리 쿼리 API 호출과 혼용하지 마세요.

다음 예시로 차이점을 확인할 수 있습니다.

lp 예시 트랜스폼

두 예시 사이에 확연한 성능 차이가 나타나며, 씬에 작은 트랜스폼 계층만 있을 때 이 차이는 더욱 커집니다.

lp 예시 트랜스폼 결과

오디오 시스템

Unity는 AudioClip 재생을 위해 내부적으로 FMOD 시스템을 사용합니다. FMOD는 자체 스레드(Thread)에서 실행되며, 이 스레드는 오디오 디코딩 및 혼합을 담당합니다. 하지만 오디오 재생이 저절로 이루어지는 것은 아닙니다. 씬에 활성화된 각 오디오 소스에 대해 메인 스레드에서 약간의 작업이 수행되어야 합니다. 또한, 오래된 휴대폰과 같이 코어가 더 적은 플랫폼에서는 프로세서 코어 확보를 위해서 FMOD의 오디오 스레드와 Unity의 메인 및 렌더링 스레드 간에 경쟁이 발생할 수 있습니다.

각 프레임에서 유니티는 모든 활성화된 오디오 소스를 반복합니다. Unity는 각 오디오 소스에 대해 오디오 소스와 활성화된 오디오 리스너(listener) 및 몇 가지 기타 파라미터 사이의 거리를 측정합니다. 이 데이터는 볼륨 감쇠(Attenuation), 도플러(Doppler) 변경 및 개별 오디오 소스에 영향을 미칠 수 있는 다른 효과를 계산하는 데 사용합니다.

흔한 문제 중 하나는 오디오 소스의 "음소거(Mute)" 체크박스에서 기인합니다. 오디오 소스를 음소거로 설정하면 관련된 컴퓨터 연산 역시 중지될 것 같지만, 이는 사실이 아닙니다.

lp 오디오 체크박스

대신에 “음소거” 설정은 거리 체크를 비롯한 기타 모든 볼륨 관련 계산이 수행된 후 볼륨 파라미터를 0으로 제한합니다. 또한 Unity에서는 음소거 처리된 오디오 소스를 FMOD로 제출하고, FMOD는 이를 무시합니다. 오디오 소스 파라미터에 대한 계산과 FMOD로의 오디오 소스 제출은 Unity 프로파일러에 AudiosSystem.Update로 표시됩니다.

이 프로파일러 표시기에 많은 시간이 할당되어 있다면, 음소거된 활성 오디오 소스가 많은지 확인해 보세요. 여러 음소거된 오디오 소스가 활성화된 경우, 음소거된 오디오 소스 컴포넌트를 음소거 대신 비활성화하거나, 오디오 소스의 게임 오브젝트를 비활성화하는 방안을 고려해 보세요. 또한 AudioSource.Stop를 호출하여 재생을 중지할 수 있습니다.

또 한 가지 방안으로는 Unity 오디오 설정에서 음성 수(Voice Count)를 제한하는 방법이 있습니다. 음성 수를 제한하려면 AudioSettings.GetConfiguration를 호출하여 가상 음성 수(Virtual Voice Count)와 실제 음성 수(Real Voice Count)의 두 가지 값이 포함된 구조를 확인합니다.

가상 음성 수를 감소시키면 FMOD가 실제로 재생할 오디오 소스를 결정하기 위하여 점검하는 오디오 소스의 수가 줄어듭니다. 실제 음성 수를 감소시키면 FMOD가 게임 오디오 생성을 위해 믹스하는 오디오 소스의 수가 감소합니다.

FMOD가 사용하는 가상 및 실제 음성 수를 변경하려면 AudioSettings.GetConfiguration에서 반환하는 AudioConfiguration 구조의 해당 값을 변경한 후, AudioConfiguration 구조를 AudioSettings.Reset에 파라미터로 전달하여 새 구성으로 오디오 시스템을 재설정해야 합니다. 참고할 점은 이는 오디오 재생을 중단하기 때문에, 로딩 화면 또는 시작 시간과 같이 플레이어가 변화를 감지하지 못할 때 진행하는 것이 좋습니다.

애니메이션

Unity에는 애니메이션 재생에 사용할 수 있는 시스템이 두 가지가 있습니다. 바로 애니메이터 시스템(Animator System)과 애니메이션 시스템(Animation System)입니다.

"애니메이터 시스템"은 게임 오브젝트를 애니메이션하기 위해 이와 연결되어 있는 애니메이터 컴포넌트와 한 개 이상의 애니메이터가 참조하는 애니메이터 컨트롤러 에셋이 포함된 시스템을 의미합니다. 이 시스템은 기존에 메카님(Mecanim)으로 불렸으며, 풍부한 기능이 있습니다.

애니메이터 컨트롤러에서는 상태(state)를 정의합니다. 이 상태는 애니메이션 클립 또는 블랜드 트리(Blend Tree)입니다. 상태는 레이어로 정리할 수 있습니다. 각 프레임에서 각 레이어의 활성화 상태를 분석하며, 각 레이어의 결과를 혼합하여 애니메이션된 모델에 적용합니다. 두 상태 간에 전환하는 경우 양쪽 상태가 모두 분석됩니다.

또 하나의 시스템은 “애니메이션 시스템”입니다. 이는 애니메이션 컴포넌트로 표현되며 매우 단순합니다. 각 프레임에서 활성화된 각 애니메이션 컴포넌트는 연결된 애니메이션 클립의 모든 커브에서 선형으로 반복 작업을 하고 커브를 분석하여 결과를 적용합니다.

이 두 시스템은 기능뿐만 아니라 기초적인 구현 디테일에서도 차이점이 나타납니다.

애니메이터 시스템은 상당히 멀티스레딩되어 있습니다. CPU 및 코어의 수에 따라 성능의 차이가 크게 나타납니다. 일반적으로 애니메이션 클립의 커브 수가 증가함에 따라 선형 미만으로 증가합니다. 그렇기 때문에 많은 커브 수가 있는 복잡한 애니메이션 분석을 잘 수행합니다. 하지만 오버헤드 비용이 꽤 높습니다.

애니메이션 시스템은 단순하기 때문에 오버헤드가 거의 발생하지 않습니다. 애니메이션 시스템의 성능은 재생되는 애니메이션 클립의 커브 수와 비례하여 선형으로 증가합니다.

두 시스템의 차이는 동일한 애니메이션 클립을 재생할 경우 가장 확연하게 드러납니다.

lp 애니메이션 클립 테스트

애니메이션 클립을 재생할 경우 콘텐츠의 복잡도와 게임이 실행되는 하드웨어에 가장 적합한 시스템을 선택하세요.

또 하나의 흔한 문제는 애니메이터 컨트롤러의 레이어를 과도하게 사용하는 것입니다. 애니메이터가 실행 중일 때 프레임마다 애니메이터 컨트롤러의 모든 레이어를 분석합니다. 여기에는 레이어 가중치가 0으로 설정되어 최종 애니메이션 결과물에 대한 가시적인 기여도가 없는 레이어도 포함됩니다.

각 추가적인 레이어는 프레임마다 각 애니메이터 컨트롤러에 연산을 추가하기 때문에 대체로 레이어를 최대한 적게 사용하는 것이 좋습니다. 애니메이터 컨트롤러에 디버그, 데모 또는 시네마틱 레이어가 있다면 이에 대한 리팩토링(Refactor)을 시도하세요. 이들을 기존 레이어에 합치거나 게임을 출시하기 전에 제거하세요.

제네릭 릭과 휴머노이드 릭 비교

Unity는 기본적으로 제네릭 릭(Generic Rig)을 사용하여 애니메이션된 모델을 임포트하지만, 개발자는 캐릭터를 애니메이션할 때 휴머노이드 릭(Humanoid Rig)으로 전환하는 것이 일반적입니다. 하지만 이는 비용이 발생합니다.

휴머노이드 릭은 애니메이터 시스템에 역운동학(Inverse Kinematics)과 애니메이션 리타게팅(Animation Retargeting)의 두 가지 기능을 추가합니다. 애니메이션 리타게팅은 여러 아바타 간에 애니메이션을 재사용하도록 하는 훌륭한 기능입니다.

하지만 역운동학이나 애니메이션 리타게팅을 사용하지 않아도 휴머노이드로 리깅된 캐릭터의 애니메이터는 프레임마다 역운동학 및 리타게팅 데이터를 계산합니다. 이는 이런 계산을 하지 않는 제네릭 릭에 비해 CPU 시간이 30%~50% 더 소요됩니다.

휴머노이드 릭의 기능을 사용하는 것이 아니라면 제네릭 릭을 사용하는 것이 좋습니다.

애니메이터 풀링

오브젝트 풀링(Object Pooling)은 게임플레이 중 성능 스파이크를 피하는 주된 전략입니다. 하지만 이전부터 애니메이터는 오브젝트 풀링과 함께 사용하기 어려웠습니다. 애니메이터의 게임 오브젝트가 활성화될 때마다 애니메이터의 애니메이터 컨트롤러를 분석하는 데 사용하기 위한 중간 데이터의 버퍼를 재빌드해야 합니다. 이는 애니메이터 리바인드(Animator Rebind)라 불리며 Unity 프로파일러에 Animator.Rebind로 표시됩니다.

Unity 2018 이전 버전에서 이를 해결할 유일한 방법은 게임 오브젝트가 아닌 애니메이터 컴포넌트를 비활성화하는 것이었으며, 이로 인해 발생하는 부작용도 있었습니다. 캐릭터에 MonoBehaviour, 메시 콜라이더(Mesh Collider), 메시 렌더러(Mesh Renderer)가 있다면 이 역시 비활성화해야 했습니다. 이를 통해 캐릭터가 사용하는 전체 CPU 시간을 절감할 수 있었지만 코드의 복잡성이 증가하여 오류가 발생하기 쉬웠습니다.

Unity 2018.1에서는 Animator.KeepControllerStateOnEnable API를 출시했습니다. 이 프로퍼티의 기본값은 false이기 때문에 애니메이터는 이전과 같이 비활성화 시에는 중간 데이터 버퍼의 할당을 취소하고 활성화 시에는 재할당합니다.

이 프로퍼티를 True로 설정하면 애니메이터는 비활성화 시 버퍼를 유지합니다. 이 경우 애니메이터를 다시 활성화할 때 Animator.Rebind가 발생하지 않으므로 애니메이터를 풀링할 수 있습니다!

리소스 더 보기
확인

유니티에서는 웹 사이트의 모든 기능을 최대로 이용할 수 있도록 쿠키를 사용합니다. 자세한 정보는 쿠키 정책 페이지를 참조하세요.