애셋, 오브젝트 그리고 직렬화 2 – 번역

애셋, 오브젝트 그리고 직렬화 2 – 번역 (Assets, Objects and serialization)

원문 링크

 

1.6 모노 스크립트 (MonoScripts)

MonoBehaviour가 MonoScript에 대한 참조(reference) 정보를 갖는다는 것을 이해하는 것이 매우 중요합니다. MonoScript는 단순히 특정 스크립트 클래스의 위치를 찾는데 필요한 정보를 포함합니다. 두 타입 모두 실행가능한 스크립트 클래스의 코드를 포함하지 않습니다.

MonoScript는 3개의 문자열 정보를 포함합니다: 어셈블리 이름(Assembly name), 클래스 이름, 네임 스페이스 이름(Namespace name).

프로젝트를 빌드하는 동안, 유니티는 Assets 폴더에 있는 스크립트 파일 전부를 모아서 Mono 어셈블리로 컴파일합니다. 구체적으로 유니티는 Assets 폴더에서 사용되는 각 언어마다 구별해서 빌드하며 Assets/Plugins 폴더에 포함된 스크립트는 별도의 어셈블리로 분리합니다. Plugins 폴더 외부의 C# 스크립트는 Assembly-CSharp.dll 에 컴파일되고, Plugins 폴더 내의 C# 스크립트는 Assembly-CSharp-firstpass.dll로 등으로 컴파일됩니다.

이렇게 컴파일된 어셈블리(사전에 빌드된 어셈블리 DLL 포함)는 유니티 어플리케이션(Application)의 최종 빌드에 포함됩니다. 이 어셈블리는 역시 MonoScript를 참조합니다. 다른 리소스와는 달리, 유니티 어플리케이션이 처음 실행되어 로드될 때 모든 어셈블리가 포함됩니다.

이 MonoScript 오브젝트 때문에, 실행가능한 MonoBehaviour 컴포넌트 타입의 코드를 애셋번들(또는 씬 또는 프리팹)에 포함할 수 없습니다. 이러한 특징 덕분에 서로 다른 MonoBehaviour가 특정 공유 클래스를 참조하는 것이 가능하도록 만듭니다. 심지어 다른 애셋번들(AssetBundle)에 있는 MonoBehaviour를 참조하는 것도 가능합니다.

 

1.7 리소스 라이프사이클 (Resource lifecycle)

UnityEngine.Object는 정의된 특정 시점에 메모리에 로드되고 해제됩니다. 로딩 시간을 단축시키고 프로그램의 메모리를 관리하기 위해서 UnityEngine.Object의 라이프사이클을 이해하는 것이 중요합니다.

UnityEngine.Object를 로드하는 두 가지 방법이 있습니다: 하나는 자동으로 로드하는 방법이고 다른 하나는 명시적으로 로드하는 방법입니다. 오브젝트는, 참조가 해제된 해당 오브젝트에 Instance ID가 매핑될 때마다 자동으로 로드됩니다. 이 오브젝트는 아직 메모리에 로드되지 않은 상태이며, 오브젝트의 원본 데이터의 위치를 찾을 수 있습니다. 스크립트를 통해서 오브젝트를 명시적으로 로드할 수 있습니다. 오브젝트를 생성하거나 리소스-로딩 API(예, AssetBundle.LoadAsset)를 호출해서 명시적으로 로드할 수 있습니다.

오브젝트가 로드되면, 유니티는 오브젝트의 참조 정보의 File GUID와 Local ID를 Instance ID로 변환합니다.

해당 Instance ID의 참조가 해제된 이후, 다음 두 가지의 조건을 만족하는 경우 오브젝트가 로드됩니다.
1. 해당 Instance ID가 현재 로드되지 않은 오브젝트를 참조할 경우
2. 해당 Instance ID가 캐시에 등록된, 유효한 File GUID와 Local ID를 가지는 경우

일반적으로 이 과정은, 해당 참조 정보가 로드되고 정보가 분석된 다음, 매우 짧은 시간에 발생합니다.

File GUID와 Local ID가 Instance ID를 가지고 있지 않은 경우 참조정보(reference)는 보존되지만 실제 오브젝트는 로드되지 않습니다. 그리고 Instance ID가, 유효하지 않은 File GUID와 Local ID를 참조하는, 아직 해제되지 않은 오브젝트에 대한 정보를 갖는 경우에, 해당 참조정보는 보존되지만 실제 오브젝트는 로드되지 않습니다. 이런 경우, 유니티 에디터에서 “(Missing)” Reference라고 표시됩니다. 실행 중인 어플리케이션이나 씬 뷰에서 “(Missing)” 오브젝트는 오브젝트의 타입에 따라서 다른 방식으로 보이게 됩니다: 메쉬는 보이지 않고, 텍스쳐는 자홍색(Magenta)로 나타납니다.

오브젝트는 다음의 세 가지 시나리오에 따라서 해제됩니다:

1. 사용하지 않는 애셋의 청소가 발생하면 오브젝트는 자동으로 해제됩니다. 다른 씬을 로드하거나 (씬을 추가로 로드하지 않는 Application.LoadLevel API를 사용했을 때) 스크립트에서 Resources.UnloadUnusedAssets API를 호출했을 때 이 프로세스가 자동으로 실행됩니다. 이 프로세스는 참조되지 않는 오브젝트들만 해제합니다: 모노 변수와 다른 오브젝트에서 참조하지 않는 오브젝트의 경우에만 해제합니다.

2. Resources 폴더에서 로드한 오브젝트는 Resources.UnloadAsset API를 호출해서 명시적으로 해제할 수 있습니다. 이 오브젝트에 대한 Instance ID는 유효한 상태로 남게되며, 역시 여전히 유효한 File GUID와 Local ID 정보를 포함합니다. 모노 변수나 다른 오브젝트에서 Resources.UnloadAsset을 통해서 해제된 오브젝트에 대한 참조 정보를 갖는 경우, 해당 참조 정보가 참조 해제되는 즉시 해당 오브젝트가 다시 로드됩니다.

3. 애셋 번들(AssetBundle)에서 로드한 오브젝트는 AssetBundle.Unload(true) API를 호출하는 즉시 자동으로 해제됩니다. 이렇게하면 해당 오브젝트의 Instance ID의 File GUID와 Local ID 참조 정보가 무효화되고 해제된 오브젝트를 참조하는 모든 참조 정보는 “(Missing)” 참조 상태로 남게됩니다. 이 때, C# 스크립트에서 이렇게 해제된 오브젝트의 프로퍼티나 메소드에 접근을 시도하면 NullReferenceException 오류가 발생합니다.

AssetBundle.Unload(false)가 호출되면, 해제된 애셋번들로부터 로드되어 아직 해제되지 않고 살아있는 오브젝트는 삭제되지 않습니다. 하지만 유니티는 해당 오브젝트의 Instance ID의 File GUID와 Local ID 참조 정보를 무효화 시킵니다. 나중에 메모리에서 해제되고 이렇게 해제된 오브젝트에 대한 실제 참조가 남아있는 경우 유니티가 이러한 오브젝트를 다시 로드할 수 없습니다.[4]

 

 

1.8 다수의 계층을 가진 오브젝트 로드하기 (Loading large hierarchies)

유니티 게임 오브젝트의 계층을 직렬화 할 때(예를 들면, 프리팹을 직렬화하는 경우), 전체 계층이 모두 직렬화된다는 점을 기억하는 것이 중요합니다.
다시 말해, 해당 계층에 있는 모든 게임 오브젝트와 컴포넌트들은 직렬화된 데이터에 개별적으로 저장됩니다. 이러한 특징은 게임 오브젝트의 계층을 로드하고 인스턴스화하는 데 걸리는 시간에 흥미로운 영향을 끼칩니다.

게임 오브젝트 계층을 생성할 때, CPU 시간은 몇가지 다른 방식으로 사용됩니다:

– 원본 데이터를 읽는 시간 (저장소로부터, 다른 게임 오브젝트로부터 등등)
– 새로운 변환 사이에 부모-자식 관계를 설정하는 시간
– 새로운 게임 오브젝트 및 컴포넌트를 인스턴스화하는 시간
– 새로운 게임 오브젝트 및 컴포넌트를 활성화하는 시간

위에서 나중에 설명한 세 가지의 시간 비용은 일반적으로, 특정 계층 구조가 기존의 계층 구조에서 복제되었는지, 저장소(애셋번들과 같은)에서 로드했는지에 관계없이 변하지 않습니다.
그러나 원본 데이터를 읽는 시간은 계층 구조에 직렬화된 컴포넌트 및 게임 오브젝트의 수에 따라서 선형적으로 증가하고, 데이터 소스의 속도도 곱해집니다.

현재 모든 플랫폼에서 저장 장치로부터 로딩하는 것보다 메모리에서 데이터를 읽는 것이 훨씬 빠릅니다. 또한 사용가능한 저장 매체의 성능 특징은 플랫폼에 따라서 다릅니다 – 데스크탑 PC는 모바일 장치보다 디스크에서 데이터를 읽는 속도가 훨씬 빠릅니다.

따라서, 저장소가 느린 플랫폼에서 프리팹을 로드할 때, 저장소로부터 프리팹의 직렬화된 데이터를 읽는 데 걸리는 시간은 해당 프리팹을 인스턴스화 하는데 걸리는 시간보다 훨씬 오래 걸릴 수 있습니다. 즉, 로딩 작업의 비용은 저장 장치의 I/O(입/출력) 시간에 의해서 좌우됩니다.

앞서 언급했듯이, 다수의 계층을 가진 단일 프리팹을 직렬화할 때, 각 게임 오브젝트와 컴포넌트의 데이터는 개별적으로 직렬화되어 저장됩니다 – 심지어 해당 데이터가 중복되는 경우에도 개별적으로 저장됩니다.
30개의 동일한 UI 요소를 갖는 UI 화면은 동일한 요소를 30번 직렬화합니다. 이 과정을 통해서, 매우 큰 바이너리 데이터 블롭이 생성됩니다.
이렇게 직렬화된 데이터를 읽을 때, 새로 생성한 오브젝트에 데이터를 전달하기 전에, 30개의 동일한 각 게임 오브젝트와 컴포넌트의 데이터를 디스크에서 모두 로드해야 합니다.
대형 프리팹을 인스턴스화 하는데 걸리는 시간 비용을 좌우하는 것은 바로 이 파일 읽기 시간입니다.

유니티가 중첩된 프리팹(Nested prefabs)을 지원할 때까지, 다수의 계층 구조를 가진 게임 오브젝트를 인스턴스화하는 프로젝트는 유니티의 직렬화 시스템과 프리팹 시스템에 전적으로 의존하는 대신,
중복되는 요소들을 프리팹으로 분리하고 런타임에, 이렇게 분리한 프리팹을 인스턴스화해서 사용함으로써 덩치가 큰 프리팹의 로딩 시간을 크게 단축할 수 있습니다.

또한, 프리팹 또는 게임 오브젝트의 계층 구조가 구성되면, 기존의 계층 구조를 복사하는 것이 저장소로부터 새 복사본을 로드하는 것보다 빠릅니다.

유니티 5.4 참고: 유니티 5.4는 메모리에 있는 트랜스폼(Transform)의 표현방식을 변경했습니다. 각 루트 트랜스폼의 전체 하위 계층 구조는 작고, 연속적인 메모리 영역에 저장됩니다.
다른 계층으로 즉시 계층구조가 변경될 새 게임 오브젝트를 생성할 때 GameObject.Instantiate 메소드를 오버로드(Overload)해서 부모(상위) 인자를 허용하도록 하는 방안을 고려하시기 바랍니다.

위에서 설명한 오버로드 메소드를 사용하면 새 게임 오브젝트에 대한 루트 트랜스폼 계층을 할당하지 않아도 됩니다. 테스트 결과, 게임 오브젝트를 인스턴스화 하는데 필요한 시간이 5-10% 정도 단축되었습니다.

 

각주 (Footnotes)

4. 오브젝트가 해제되지 않고 런타임에 메모리에서 제거되는 가장 일반적인 경우는 유니티가 그래픽 컨텍스트(Context)에 대한 제어권을 잃었을 때 발생합니다.
예를 들어, 모바일 앱이 실행 중지되어 앱이 백그라운드에서 강제 실행되는 경우 이러한 현상이 발생할 수 있습니다.
이 경우, 모바일 OS는 주로 GPU 메모리에서 모든 그래픽 리소스를 제거합니다.
응용 프로그램이 앱이 다시 실행되면(foreground로 돌아오면), 유니티는 씬을 다시 렌더링하기 전에, 모든 텍스쳐, 쉐이더, 메쉬를 GPU에 다시 업로드해야 합니다.

 

내용 끝까지 읽어주셔서 감사합니다.
배너 클릭은 저에게 많은 힘이 됩니다.
감사합니다 🙂

 

RonnieJ

프리랜서 IT강사로 활동하고 있습니다. 게임 개발, C++/C#, 1인 기업에 관심이 많습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

Please turn AdBlock off

Notice for AdBlock users

Please turn AdBlock off