2019.3 버전에 들어서면서 Addressable Asset System이 정식으로 릴리즈 되었다.

Addressable Asset System은 유니티의 새로운 에셋 관리 시스템인데,

기존의 Resources, Remote AssetBundle의 통합하는 새로운 시스템이다.

 

잘 사용만 한다면, 기존에 에셋 로딩 방식이 가지던 단점들을 보완하는 꽤 좋은 시스템이다.

그래서 앞으로 게임 개발 시엔 이 Addressable Asset System이 기본이 될 것이며 유니티도 이 시스템을 밀어줄 것이라 생각한다.

(따지고 보면, 기존의 에셋 번들 시스템을 개선한 버전이기는 하다.)

 

시대에 뒤처지지 않기 위해, 나도 이것저것 테스트해보는 중이다.

테스트해보면서 알게 된 몇 가지 특징들이 있는데,

잊지 않기 위해서 여기에 적어두려고 한다.

 

(이 부분은 어느 정도 Addressable Asset System에 대해 이해가 있어야지 이해가 쉬울 것이다.)

  • Addressable에서 에셋을 분류하는 단위인 Group은 기존의 번들 개념과 유사하다.
    • 기존 에셋에 번들 이름을 부여해서, 나중에 번들 이름 단위로 데이터를 생성하듯이, 기본적으로 어드레서블은 그룹 단위로 에셋 번들을 만든다.
    • 그래서 기존의 에셋번들 시스템에서 어드레서블로 컨버팅을 하면 에셋 번들 이름이 그룹으로 처리되면서, 그룹 별로 에셋들이 분류된다.
    • 옵션 변경해서 그룹 단위가 아닌 개별 에셋, 혹은 레이블 단위로 에셋 번들을 만들 수 있다.
  • Addressable에서 LoadAsset과 DownloadDependencies는 Key 또는 Label 값을 파라미터로 받는다.
    • 이 부분이 아직 나에게 낯선 개념인데, 에셋을 그룹 단위로 로드할 수 없다. 에셋의 Key값이나, Label을 통해서만 로드할 수 있다. 여기서 레이블이란, 일종의 Tag개념으로 에셋마다 여러 개 레이블을 가질 수 있다. 그래서 레이블로 로딩 시, 해당 레이블을 가진 모든 데이터를 로드한다.
    • 이유는 위에서 설명한 옵션 때문인데, 에셋 번들이 무조건 그룹 단위로 만들어지는 것이 아니기 때문에, 그룹 단위로 처리하는 함수는 없다.
  • LoadAsset, Instantiate은 에셋이 리모트 에셋일 경우, 다운로드를 한다
    • 굳이 다운로드 함수를 호출하지 않아도, 로컬에 캐싱된 에셋 번들이 없다면 자동으로 다운로드를 해서 캐싱을 한다.
    • 로컬 캐싱관련 옵션을 끄면 매번 로드할 때마다 다운로드를 한다.
  • LoadAsset, Instantiate, DownloadDependencies를 이용해 에셋을 로드하면, 관련 의존된 에셋 또한 알아서 다운로드하고 로드해준다.
    • 에셋 하나를 로드하는데 의존 에셋이 있다면 같이 다운로드한다. 그게 설령 의존에 의존에 의존 에셋이라도.
    • 예를 들어, 리모트 번들 A에 메쉬 Prefab이 있고, 리모트 번들 B에 메쉬의 Material이 있고, 리모트 번들 C에 머테리얼의 Texture가 있으면, Prefab 에셋을 로드할 때, 자동으로 번들 B와 C까지 알아서 같이 로드한다.
  • 그룹 단위의 처리 방식이 아니기 때문에 새로운 방식의 에셋 데이터 관리 방향을 제시할 수 있다.
    • 기존 게임들은 게임 시작 시에 필요한 데이터를 사전에 전부 다운로드하도록 하지만, 위에 설명한 어드레서블의 특징 덕분에 그룹 단위로 에셋 데이터를 관리하지 않아도 되기 때문에, 유저가 게임을 플레이를 하면서 필요한 데이터만 다운로드하게 할 수 있다.
  • LoadAsset로 불러온 에셋은 사용 후 Release를 호출해줘야한다.
    • 기존 변수를 Null로 처리하는 것 또한 잊으면 안 된다.
  • Instantiate으로 생성한 오브젝트는 ReleaseInstance를 호출해줘야 한다.
    • 다만, 호출하면, 오브젝트가 Destroy 되므로 풀링을 했다면, 더 이상 필요 없어진 시점에 해야 할 것이다.
    • Release를 호출해도 똑같이 작동한다.
    • 반대로 LoadAsset으로 불러 온 Prefab 또한 ReleaseInstance로 처리해도 동일하게 처리된다.
  • LoadAsset으로 불러온 Prefab으로 생성한 인스턴스는 Release 대상이 아니다.
  • AsyncOperationHandle로 Release를 하면 로드/생성된 에셋 또한 릴리즈 된다.
    • 때문에 Instantiate의 AsyncOperationHandle을 이용해 해제하면, 인스턴스를 Release 한 것처럼 오브젝트가 Destroy 된다.
  • 에셋끼리 동일한 키값을 가질 수 있다. 해당 키로 에셋을 로드할 때, LoadAssets로 에셋을 불러오면, 배열로 로드할 수 있다.
  • LoadAssets으로 한 번에 여러 에셋을 불러올 때 특정 데이터 타입으로 하면, 같은 값(키 또는 레이블)을 가진 에셋 중에 해당 타입을 가진 에셋만 불러온다.
  • LoadAssets으로 한 번에 여러 에셋을 불러올 때 object로 타입을 정하면, 같은 값(키 또는 레이블)을 가진 모든 에셋을 불러온다.

 

기능을 더 확장하고 활용하려면, Provider 같은 인터페이스들을 상속하고 커스터마이즈 하는 법까지 알아야 할 텐데.

그 부분은 나에게 너무 어렵다...

 

이 시스템을 이용해서 어떻게 번들 데이터들의 버전 업데이트 시스템을 만들 수 있을지는 아직 고민해보지 않아서,

이건 차차 업데이트할 예정이다.

 

 

'Unity' 카테고리의 다른 글

왜 Unity의 GC(Garbage Collector)는 무식할까  (0) 2020.04.24
Script Execution Order  (0) 2020.04.05

성능에 조금이라도 관심 있는 사람은 GC에 대하여 들어본적이 있을 것이다.

그래서 유니티의 GC에 대해 공부하다보면, 발견할 수 있는 점이 .NET의 GC와 Unity의 GC가 다르다는 점이다.

 

필자는 유니티의 GC를 언뜻 듣고, .NET의 GC를 공부하고 나서,

당연히 유니티도 C#을 쓰니 같은 GC겠거니 했는데, 그게 아니었다.

 

이번 글에선 왜 그런지를 알아보려고 한다.

 

.NET Framework Garbage Collector

일단, 각 GC가 어떻게 동작하는지 심플하게 설명해보자.

.NET Framework의 GC는 세월이 흐르면서 여러 방법으로 업그레이드 되어 왔다.

 

기본적으로 .NET의 GC는 세대(Generation) 방식을 사용한다.

할당된 메모리별로 0~2세대까지 세대를 부여하여,

통계적으로 (자기네들이 생각하기에) 최적의 타이밍에 필요한 메모리를 해제할 수 있도록 관리한다.

 

그리고 힙영역은 SOH(Small Object Heap)과 LOH(Large Object Heap)으로 구분하여,

큰 메모리를 할당하고 해제할 때의 부담을 줄도록 만들었다.

 

SOH에는 작은 메모리들이 할당되고, GC가 일어날 때, 정리된 세대의 메모리의 재정렬이 일어난다.

LOH에는 큰 메모리들이 할당되고(기준은 85,000바이트라고 한다), GC가 일어날 때,

성능 저하를 피하기 위해서 메모리 재정렬은 하지 않는다.

 

그리고 GC시에는 GC 작업으로 인한 부하를 더 줄이려고, 전용 쓰레드를 쓰고,

또 거기에 로직을 넣고 적절한 시기에 쓰레드를 돌리는 등 여러가지가 있다.

 

Unity Garbage Collector

유니티의 GC는 꽤나 단순무식하다.

그말인즉, 성능은 상대적으로 좋지 않다는 뜻이다.

 

세대 구분? 없다.

SOH와 LOH? 없다.

메모리 재정렬? 없다.

 

그냥 하나의 힙 메모리 영역만 있으며 메모리가 할당되면 죄다 여기에 할당한다.

해제 시 재정렬을 하지 않아, 할당과 해제를 자주하면 메모리에 구멍이 송송 나있어서,

공간은 많은데 메모리 할당을 못하는 경우도 있다.

물론, 내부적으로 메모리가 부족하면 힙 영역을 늘리지만, 결국 근본적인 문제는 해결되지 않고

게임은 메모리 먹는 하마가 된다.

 

게다가 GC시엔 그냥 앱의 다른 동작을 정지시키기 때문에 렉도 심하다.

 

이정도면 너무하지 않나 싶을 정도이다.

 

왜?

왜 유니티는 이런 구린 GC를 쓰는걸까?

궁금해서 구글링을 해봤다.

 

그리고 StackOverflow에서 흥미로운 코멘트를 읽었다.

(검증은 안했으므로 카더라 일수도 있는 점 양해바랍니다.)

 


It was in early 2008 that Unity and Mono announced their collaboration, and at that time Unity licensed the Mono runtime (GPL covered for open source usage) so as to embed it. And the Boehm GC was the primary GC in Mono then.
Time passed and Mono 4.x/5.x by default uses SGen GC with generational/compacting features. However, Unity did not want to pay the licensing again. Thus, you see the documentation remains it was.

Microsoft acquired Xamarin in 2016, and hence gained control of Mono core assets. It republished the code base under MIT, so solving the licensing issue for ever. Unity joined .NET Foundation and started to work with Microsoft/Xamarin to incorporate the latest Mono runtime into the game engine.

That effort is still undergoing and should soon reach maturity (currently an experimental feature).

BTW, Unity cannot use the standard .NET GC yet. Microsoft does not open source its GC in .NET Framework, but a version in .NET Core. That GC is different from Mono's, and would require more efforts to be embedded into Unity. I guess that's why Mono 5 was chosen to be integrated right now. Maybe in the future Unity would migrate to the .NET Core GC.

https://stackoverflow.com/questions/46574407/unitys-garbage-collector-why-non-generational-and-non-compacting

 

Unity's garbage collector - Why non-generational and non-compacting?

I've just read in Unity's docs that Unity’s garbage collection – which uses the Boehm GC algorithm – is non-generational and non-compacting. “Non-generational” means that the GC must sweep throu...

stackoverflow.com


대충 요약하자면

'과거에 유니티는 가난해서 최신 Mono의 GC를 적용할 돈이 없었어서 초장기 계약한 구식 GC를 계속 사용 중이었다. (구식 C# 버전과 함께)

이제는 Xamarin(Mono개발사)이 마이크로소프트에 인수되어, Mono가 MIT 라이센스로 풀렸기 때문에, 유니티는 최신 버전을 적용할 수 있게 되었고, 열심히 작업 중이다'

 

결국, 유니티는 가난했고, 마소의 자마린 인수 후 오픈소스 정책으로 이제야 적용하는 중이라는 것이다.

 

그래서 찾아보면 현재 유니티가 점진적 가비지 콜렉션을 조금씩 시범 적용 중인 것을 알 수 있는데,

그 기능이 적용된 이유가 아마 이것과 관련이 있지 싶다.

 

결론?

마소를 찬양하라...

아마 마소가 자마린 인수 안했으면, MIT로 전환하지 않았으면,

우리는 아직도 구닥다리 GC와 C#을 사용하고 있었을지도 모른다.

 

 

'Unity' 카테고리의 다른 글

Addressable Asset System에 대한 자잘한 특징??  (0) 2020.05.19
Script Execution Order  (0) 2020.04.05

요즘 유니티는 예전 유니티와 많이 다르다. 예전에 뭔가 2% 부족하다고 느꼈던 기능들이 하나씩 추가되고 또 출시될 예정이라 꽤나 정교한 엔진이 돼가고 있음을 자주 느낀다.

그래서 가능하면 최신 기능을 위해 어지간하면 업데이트를 자주 해주는 것이 좋다.

(물론 기업 같은 경우엔 LTS 버전을 사용하는게 훨씬 안정적이고 좋긴 하지만.)

 

본론으로 들어가자면,

사실 지금 소개할 기능은 2019버전뿐만 아니라 이전에도 있던 기능이다.

Unity 메뉴얼 상으로 2017 버전부터 존재한 걸 보면 아마 그때부터 추가된 기능일 것이다.

 

유니티에선 여러 오브젝트에 붙어 있는 여러 컴포넌트의 Awake나 Start 같은 유니티 이벤트 함수들이 호출될 때,

어떤 오브젝트의 어떤 컴포넌트가 먼저 호출 될지는 정해져 있지 않다.

 

그래서 게임 첫시작 할 때, 가장 우선적으로 처리해야 하는 시스템, 데이터 초기화 같은, 다른 클래스들보다 먼저 호출 되어야하는 주요 클래스들의 호출 순서를 컨트롤할 수 없어서 다른 방법으로 이 호출 순서가 꼬이지 않게 처리해야 했다.

 

바로 이런 문제를 간단하게 해결 할 수 있는 방법이 있는데, 그게 바로 Script Execution Order이다.

단어 그대로 스크립트 호출 순서를 컨트롤하는 기능이다.

 

예시용 프로젝트의 셋팅 사진을 예로 들자면.

빨간 박스가 작성자가 설정한 스크립트 클래스 명이고, 파란 박스가 유니티가 자체 설정, 아래 Default Time이 일반적인 호출 순서이다.

각 항목 우측에 있는 숫자는 우선순위를 뜻하고, 0보다 낮을수록 먼저, 0보다 높을수록 나중에 호출된다는 뜻한다.

때문에 원한다면, 일반적인 default time 보다 더 늦게 호출하도록 설정할 수도 있다,

 

우선순위 값을 변경하면 항목의 위치가 값에 맞게 자동으로 변경되고, 각 항목의 좌측의 '='를 이용해 드래그엔드랍으로 우선순위 값을 자동으로 바꿀 수도 있다.

 

사진에 작성자가 적용한 항목들은 다음과 같다.

SimpleDI.PersistentContext: 게임에 전역적으로 사용될 클래스들을 초기화하는 컴포넌트

SimpleDI.SceneContext: 씬에 종속적인 클래스들을 초기화하는 컴포넌트

SimpleDI.MonoLifeCycle: Monobehaviour를 상속하지 않은 클래스들에 대해 Update같은 함수를 호출해주기 위한 컴포넌트

 

풀어서 설명하자면, 전역 -> 씬 -> 업데이트 시작으로 시스템이나 데이터 초기화의 순서가 꼬이지 않도록 설정해놓은 것이다.

 

Script Execution Order은 앞서 설명한대로,

유니티 이벤트 함수에 대하여 어떤 컴포넌트들이 우선 혹은 나중에 호출될지를 결정하는 셋팅이기 때문에,

설정할 클래스는 당연히 컴포넌트여야 한다.

 

이 셋팅은 스크립트로도 설정할 수 있어서, 스크립트로 특정 스크립트의 우선순위 값을 얻어오거나, 바꿀 수 있다.

 

void MonoImporter.SetExecutionOrder(MonoScript script, int order);

int MonoImporter.GetExecutionOrder(MonoScript  script);

 

마지막으로 알아둘 점과 사용 시 주의할 점이 있다.

 

알아둘 점

  • 이 셋팅은 게임 시작부터 끝까지 전역적으로 영향을 미침
  • 각각의 순서는 클래스의 메타 파일에 저장되기 때문에 메타 파일이 지워지면 셋팅값이 사라짐

주의할 점

  • 남발하지 말 것
  • 정말 중요한 코어 컴포넌트 몇 개만 사용할 것
  • 평범한 컴포넌트한테는 우선순위 값을 주지 말 것

이유를 설명하자면, 이 자체가 클래스들에게 순서 의존성을 부여하는 것이기 때문에, 나중에 우선 호출되는 컴포넌트가 나중 호출되는 컴포넌트의 우선 호출을 요구하는, 의존성 순환구조가 되어버릴 문제가 있기 때문이다. 이렇게 되면 나중에 구조를 수정하기 힘들어질 수 있다.

그래서 가능하다면 예시 사진과 같이 중요 컴포넌트에게만 호출 순서를 부여하고 내부적으로는 이 컴포넌트들을 이용하여 순서를 조절할 수 있게 하는 것이 좋다.

+ Recent posts