Unity 검색

C# 데이터 구조 및 Unity API를 통한 최적의 작업 수행

Last updated: December 2018

이 페이지의 내용: Unity API 및 데이터 구조의 최적의 작업 수행을 위한 해결책을 다룹니다. 파티클 시스템 API를 통한 재귀적 반복, 잦은 메모리 할당, 배열을 반환하는 API, 잘못된 데이터 구조 선택, Dictionary 오용으로 인한 과다한 오버헤드 발생 등의 문제를 다룹니다.

이러한 팁의 상당수는 실제로 적용했을 때 추가적인 복잡성을 유발하여 더 많은 유지 관리 업무 및 추가적인 버그가 발생할 수 있습니다. 이러한 솔루션을 적용하기 전에 먼저 코드를 분석해 보세요.

모든 팁은 유나이트 강연인 유니티 털어보기: 성능 향상을 위한 팁(Squeezing Unity: Tips for raising performance)에서 제공합니다. Unity 아키텍처를 데이터 중심 설계로 발전시키는 방법에 관한 이안의 새로운 팁을 알아보려면 발전하는 베스트 프랙티스를 참조하세요.

Unity API

문제: 파티클 시스템 API 실행이 반복됨

Start, Stop, IsAlive()와 같은 파티클 시스템의 기본 API 중 하나를 호출하는 경우, 파티클 시스템 계층 구조의 자식 요소가 다음에 해당하는 경우에도 API가 반복적으로 실행됩니다.

  • API는 파티클 시스템의 Transform에서 모든 하위 요소를 찾고 모든 Transform에서 GetComponent를 호출하며, 파티클 시스템이 있는 경우 적절한 내부 메서드를 작동합니다.
  • 이러한 하위 Transform에 자체 하위 요소가 있는 경우 API는 Transform의 계층 구조 맨 하단까지 동작을 반복하여 모든 하위 요소, 하위 요소의 하위 요소 등까지 모두 처리합니다. 이러한 과정은 Transform 계층 구조가 깊은 경우 문제가 될 수 있습니다.

솔루션:

이러한 API에는 true가 기본값으로 설정된 withChildren 파라미터가 있습니다. 반복 동작을 제거하려면 값을 false로 설정하세요. withChildren이 false로 설정되면 직접 호출한 파티클 시스템만 변경됩니다.

문제: 동시에 시작하고 정지하는 여러 파티클 시스템

많은 경우 아티스트들은 여러 하위 Transform에 걸친 파티클 시스템이 포함된 시각적 효과를 만듭니다. 이러한 파티클 시스템은 동시에 시작하고 정지하는 것이 좋습니다.

솔루션:

초기화 시간에 GenComponent 목록을 호출하여 파티클 시스템의 목록을 캐싱하는 MonoBehaviour를 만드세요. 그런 다음 파티클 시스템을 변경해야 할 때 각 파티클 시스템에 대해 Start, Stop 등을 번갈아 호출하고 반드시 false를 withChildren 파라미터로 전달하세요.

문제: 잦은 메모리 할당

C# 클로저가 로컬 변수를 둘러싸는 경우 C# 런타임은 변수 추적을 위해 힙에 레퍼런스를 할당해야 합니다. Unity 5.4~2017.1 버전에는 호출하는 경우 내부에서 클로저를 사용하는 파티클 시스템 API가 몇 가지 있습니다. 이러한 Unity 버전에서는 이미 파티클 시스템이 정지된 경우에도 모든 정지 및 시뮬레이션(Stop and Simulate) 호출이 메모리를 할당합니다.

솔루션:

모든 파티클 시스템 API는 유니티의 공용 API 대다수와 마찬가지로 API의 실제 동작을 수행하는 내부 기능을 둘러싼 C# 래퍼 기능일 뿐입니다. 이러한 내부 기능 중 일부는 순수한 C#이지만, 대다수는 유니티 엔진의 C++ 코어를 호출합니다. 파티클 시스템 API의 경우 모든 내부 API는 편의를 위해 “Internal_” 뒤에 공개 기능의 이름을 붙여 명명됩니다. 예를 들면 다음과 같습니다.

work-optimally-with-Unity-APIs-table-graph

이 외에도 여러 가지 형태가 있습니다.

유니티의 공개 API에서 클로저를 통해 내부 헬퍼를 호출하도록 하는 대신 확장 메서드를 작성하거나 리플렉션을 통해 내부 메서드를 호출하고 나중에 사용할 수 있도록 레퍼런스를 캐싱할 수 있습니다. 다음은 몇 가지 관련 기능입니다.

work-optimally-with-Unity-APIs-functions-signatures

위의 모든 인수는 정지하거나 시뮬레이션을 수행하려는 파티클 시스템에 대한 레퍼런스인 첫 번째 인수를 제외하고 모두 공용 API와 동일한 의미를 지닙니다.

문제: 배열을 반환하는 API

사용자가 배열을 반환하는 유니티 API에 액세스할 때마다 API는 해당 배열의 새로운 사본을 할당합니다. 이러한 동작은 배열을 반환하는 기능을 사용할 때와 배열을 반환하는 속성을 사용할 때 모두 발생합니다. 이의 예는 Mesh.vertices로, Mesh.vertices에 액세스하면 메시 버텍스의 최신 사본을 획득합니다. 현재 프레임에서 사용자가 수행하는 모든 터치의 사본을 제공하는 Input.터치도 이러한 예입니다.

솔루션:

이제 많은 유니티 API의 경우 비할당 버전이 제공되므로 이러한 버전을 대신 사용하세요. 예를 들어, Input.터치 대신 Input.touchCount를 사용하면 됩니다. 버전 5.3 이후 모든 물리 쿼리 API의 비할당 버전이 도입되었습니다. 2D 애플리케이션의 경우 Physics2D 쿼리 API의 비할당 버전도 제공됩니다. 여기에서 유니티 Physics API의 비할당 버전에 대해 자세히 알아보세요.여기를 참조하세요.

마지막으로 GetComponents 또는 GetComponentsInChildren을 사용하는 경우 이제 일반적인 템플릿 목록을 허용하는 버전이 제공됩니다. 이러한 API는 목록을 GetComponents 호출의 결과로 채웁니다. 이러한 과정은 일부 할당을 포함할 수 있습니다. 즉, GetComponents 호출의 결과가 목록의 용량을 초과하는 경우 목록의 크기가 조정됩니다. 단, 목록을 재사용하거나 풀링하는 경우 적어도 할당의 빈도는 애플리케이션의 수명에 걸쳐 감소합니다.

최적의 데이터 구조 사용

문제: 잘못된 데이터 구조 선택

사용하기 편리한 데이터 구조 대신 작성 중인 알고리즘이나 게임 시스템에 가장 적합한 성능 특성을 갖춘 데이터 구조를 선택하세요.

솔루션:

배열이나 리스트에 인덱싱하는 작업은 약간의 내부 요소만이 필요하며 상수 시간에 발생하므로 매우 적은 비용이 소요됩니다. 따라서 배열이나 리스트에 임의로 인덱싱하거나 동작을 반복 수행하는 데에도 매우 적은 비용이 소요됩니다. 각 프레임마다 몇 가지를 반복 수행하는 경우 배열이나 목록을 우선 활용하세요.

상수 시간 추가나 제거가 필요한 경우 Dictionary 또는 Hashset을 사용하는 것이 좋습니다(아래에서 자세한 내용 참고).

데이터 조각이 일방향적으로 다른 조각에 연결되는 키-값 방식으로 데이터를 연계하는 경우, Dictionary를 사용하세요.

Hashset 및 Dictionary에 대한 자세한 내용: 두 가지 데이터 구조 모두 해시 표를 기반으로 합니다. 해시 표에는 여러 버킷이 있으며, 기본적으로 각 버킷은 특정한 해시 코드가 지정된 값을 포함한 목록이라는 점에 유의하세요. C#의 경우 이러한 해시 코드는 GetHashChode 메서드에서 전송됩니다. 일반적으로 특정 버킷의 값 수는 Hashset 또는 Dictionary의 총 크기보다 훨씬 작으므로 버킷에서 요소를 추가하고 제거하면 리스트나 배열에서 요소를 임의로 추가하거나 제거할 때보다 상수 시간에 훨씬 더 가까워집니다. 해시 표의 용량 및 해시 표에 저장하는 항목 수에 따라 미세한 차이가 발생합니다.

같은 이유로 Hashset 또는 Dictionary에서 특정 값의 존재 여부를 확인하는 것 또한 매우 쉽습니다. 값의 해시 코드를 나타내는 버킷에 포함된 비교적 적은 수의 값에서 신속하게 해당 값을 찾을 수 있습니다.

문제: Dictionary의 오용으로 인해 과다한 오버헤드 발생

프레임마다 데이터 항목 쌍을 반복 수행하려는 경우 대개는 편리함 때문에 Dictionary를 사용합니다. 하지만 이러한 경우 해시 표를 통해 반복을 수행한다는 문제가 있습니다. 즉, 값 포함 여부와는 상관없이 해당 해시 표의 모든 버킷에 대해 반복을 수행해야 합니다. 특히 적은 수의 값을 포함하는 해시 표에 대해 반복을 수행하는 경우 이로 인해 상당한 오버헤드가 발생할 수 있습니다.

솔루션:

대신 구조나 튜플을 만든 다음, 데이터 관계를 포함하는 구조나 튜플의 목록 또는 배열을 저장하세요. Dictionary 대신 이러한 목록/배열에 대해 반복을 수행하면 됩니다.

이안(Ian)의14분 25초 지점을 보시면 InstanceID-키 Dictionary를 사용하여 오버헤드를 감소할 수 있는 경우와 방법에 관한 팁을 확인할 수 있습니다.

문제: 여러 가지 우려가 있는 경우

물론 실제 상황에서는 대부분의 경우 문제에 중복되는 여러 요구가 존재하며, 하나의 데이터 구조로는 모든 요구 사항을 충족할 수 없습니다.

흔한 예로업데이트 관리자를 들 수 있습니다. 업데이트 관리자란 오브젝트(보통 MonoBehaviour)가 게임 내 여러 시스템에 업데이트 콜백을 배포하는 설계 프로그래밍 패턴입니다. 업데이트를 수신하려는 시스템은 업데이트 관리자 오브젝트를 통해 업데이트 관리자를 구독합니다. 이러한 시스템에는 적은 오버헤드가 발생하는 반복, 상수 시간 입력 및 상수 시간 복제 검사가 필요합니다.

솔루션: 두 가지 데이터 구조 사용

  • 반복을 위한 목록이나 배열을 유지 관리하세요.
  • 리스트를 변경하기 전에 Hashset(또는 다른 인덱싱 세트)를 사용하여 추가하거나 제거하는 항목이 실제로 존재하는지 확인하세요.
  • 제거가 우려되는 경우 링크 리스트나 침입 링크 리스트 구현을 고려하세요. 단, 이러한 요소를 구현하는 경우 메모리 사용량이 증가하며 반복 오버헤드도 소폭 증가할 수 있습니다.
리소스 더 보기
확인

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