원문 : https://blogs.unity3d.com/kr/2016/12/02/unity-expands-2d-offerings-with-anima2d/

유니티에서 우리는 개발자들을 도울 수 있는 방법과 가능한 최고의 툴을 제공할 수 있는 새로운 방법들을 항상 모색하고 있습니다. 그 목적을 당성하기 위해서, 유명한 뼈대 애니메이션 소프트웨어인 Anima2D가 2017년 1월에 무료로 유니티 개발자 커뮤니티에 올라오게 됩니다! 이 강력한 툴과 이 툴의 개발자 중 한명인 Sergi Valls를 우리 2D 팀에 맞이하게 된 것을 환영합니다.

2D 제작 경험의 개선

우리는 개발의 민주화에 대한 목표를 위해 공헌하고 있고, Anima2D는 우리의 툴셋과 2D 공간에서의 작업에 초점을 맞추고 있는 개발자들을 위한 워크플로우를 개선하는 데에 중요한 한 부분이 될 것 입니다. Sergi의 경험과 결합된 Anima2D의 2D 애니메이션과 2D 캐릭터 툴에 대한 독특한 접근 방식은 이러한 노력의 중추적인 부분이 될 것 입니다.

Anima2D

Anima2D 특징

  • 2D Bones
  • Sprite를 메쉬로 변환
  • SpriteMesh 에디터
  • 자동 weights
  • Inverse Kinematics
  • 자세를 저장 / 로드
  • 아틀라스 호환성
  • Animation을 Bones으로 굽기
  • Onion Skin
  • Avatar Masks 생성

유니티로 작업

이 에셋을 더 개선하기 위한 노력으로써, 12월에는 에셋 스토어에서 Anima2D를 일시적으로 내릴 것 입니다. 그리고 2017년 1월에 Unity 커뮤니티에 무료로 선보이게 되고, 이 때 문서에 수정이 있을 것이며 개발자들이 최고의 프로젝트를 만들 수 있도록 도와주기 위해 필수 지원을 할 예정입니다. 우리는 이것의 가치를 믿고, 여러분이 가능한 최대로 Anima2D를 활용할 수 있도록 도와드리고 싶기 때문에 이런 일을 하고 있습니다.

이 에셋에 대한 전체 문서를 보고 싶으시면, Anima2D User Guide 문서를 참고하세요.

Anima2D를 유니티 개발자 커뮤니티에 가져오는데에 있어서 Mandarina Games의 Joaquim Virgili와 Sergi Valls에게 감사를 표합니다.

이 글은 유니티 자습서의 글을 번역한 글 입니다. 원문은 여기에서 확인할 수 있습니다.



이 글에서는 게임에서 물리를 사용할 때의 모범 사례를 약간 살펴볼 것이고, 왜 이렇게 써야하는지에 대한 증명도 다룰 것 입니다.

Layer와 Collision Matrix

따로 설정하지 않은 모든 게임 오브젝트는 기본적으로 Default 레이어로 생성이 되고 이는 모든 것과 충돌됨을 의미합니다. 이는 매우 비효율적입니다. 어떤 것끼리 서로 충돌을 할 지 설정을 하세요. 이렇게 하기 위해서는 오브젝트의 타입에 맞게 다른 레이어를 설정해야 합니다. 새로운 레이어가 생길 때마다 Collision Matrix에 새로운 행과 열이 생기게 됩니다. 이 매트릭스는 레이어들 간의 상호작용을 정의하게 됩니다. 기본적으로, 새로운 레이어를 추가하면, Collision Matrix는 새 레이어를 다른 모든 레이어와 충돌하게 끔 설정합니다. Collision Matrix를 제대로 설정함으로써 불필요한 충돌 처리를 피할 수 있습니다.

시연 목적으로 간단한 데모를 만들었는데, 여기에서는 하나의 박스 컨테이너 안에 2000 개의 오브젝트(빨간색 1000개와 초록색 1000개)를 생성합니다. 녹색 오브젝트는 같은 녹색 오브젝트와 컨테이너 벽과만 상호 작용을 해야하고, 빨간색의 경우도 마찬가지 입니다. 테스트 중 하나에서, 모든 인스턴스가 Default 레이어에 속하고, 충돌 리스너에 있는 게임 오브젝트 태그를 문자열 비교함으로써 상호 작용이 이루어지게 됩니다. 다른 테스트에서는, 각 오브젝트 타입은 각각의 레이어로 설정되고, Collision Matrix를 통해서 각 레이어의 상호작용 설정을 하게 됩니다. 이 경우에는 필요한 충돌만 발생하기 때문에 문자열 테스트가 필요없습니다.

그림 1 : Collision Matrix 설정
그림 1 : Collision Matrix 설정

아래 이미지는 시연에서 가져온 결과입니다. 이 시연에는 간단한 매니저 클래스가 있는데, 이 클래스는 충돌의 수를 세고, 5초 후에 자동으로 멈추게 합니다. 공통의 레이어를 사용하는 경우에 불필요한 여분의 충돌이 발생하는 부분이 매우 인상깊습니다.

그림 2 : 5초 동안의 충돌
그림 2 : 5초 동안의 충돌

더 자세한 데이터를 위해 물리 엔진의 프로파일러 데이터도 캡쳐했습니다.

그림 3 : (공통 레이어 vs 분리한 레이어) 물리 프로파일러 데이터
그림 3 : (공통 레이어 vs 분리한 레이어) 물리 프로파일러 데이터

프로파일러 데이터에서 볼 수 있듯이, 물리에서 CPU 사용량의 차이가 큰 것을 확인할 수 있습니다. 단일 레이어를 사용하게 되면 평균 27.7 ms 정도이고, 레이어를 분리해서 사용하게 되면 평균 17.6 ms 정도 입니다.

RAYCASTS

레이캐스팅은 물리 엔진에서 굉장히 유용하고 강력한 도구입니다. 이는 우리가 특정 방향으로 특정 길이의 광선(ray)을 쏠 수 있도록 해주고, 이를 통해서 어떤 것과 부딪혔는지를 알 수 있습니다. 하지만, 이는 비싼 동작입니다 ; 이 성능은 광선의 길이와 씬에서의 콜라이더의 타입에 크게 영향을 받습니다.

이를 사용하는 데에 도움이 될 수 있는 몇가지 힌트를 드리겠습니다.

  • 이는 명확합니다. 하지만, 작업을 진행하기 위한 광선은 최소한으로 사용하십시오.
  • 필요 이상의 광선 길이를 확장하지 마십시오. 광선이 길어질 수록, 더 많은 오브젝트가 테스트 되어야 합니다.
  • FixedUpdate() 함수 안에서 Raycast를 사용하지 마십시오. 가끔은 Update() 에서의 사용도 과잉 사용이 될 수 있습니다.
  • 여러분이 사용하고 있는 콜라이더의 타입을 알고 계십시오. 메쉬 콜라이더에 대한 레이캐스팅은 정말 비쌉니다.
    • 좋은 해결책은 자식에 기본 콜라이더(primitive colliders)를 사용하고, 대략적으로 매쉬와 비슷한 모양이 되도록 노력하는 것 입니다. 부모 Rigidbody 밑에 있는 모든 자식 콜라이더는 합성 콜라이더처럼 동작할 것 입니다.
    • 메쉬 콜라이더의 사용이 절실한 경우에는 최소한 그들을 볼록하게 되도록 사용하십시오.
  • 광선이 무엇을 맞춰야하는지를 확실히 하고, 항상 raycast 함수에 레이어 마스크를 지정하도록 하십시오.
    • 이는 공식 문서에 잘 설명되어 있지만 raycast 함수에 사용해야할 값은 layer id가 아닌 bitmask 값 입니다.
    • 따라서 만약 레이어 아이디가 10인 오브젝트를 맞추고 싶다면, 10이 아닌 1<<10 (1을 왼쪽으로 10만큼 비트 이동) 을 사용하세요.
    • 레이어 10이 아닌 나머지 모든 것들을 맞추고 싶은 경우에는, 비트 보수 연산자인 ~을 사용하세요. 그러면 비트 마스크의 각 비트의 역을 구할 수 있습니다.


어떤 오브젝트가 녹색 박스에서 충돌하는 광선을 쏘는 그런 간단한 시연을 하나 만들었습니다.

그림 4 : 간단한 Raycast 시연 씬
그림 4 : 간단한 Raycast 시연 씬

이 시연에서 저는 앞에서 말한 내용을 지지할 수 있는 프로파일러 데이터를 뽑아내기 위해 광선의 수와 길이를 조절해 보았습니다. 아래의 그래프를 보면 광선의 수와 길이가 성능에 얼마나 영향을 미치는지 알 수 있습니다.

그림 5 : 성능에 영향을 미치는 광선의 수
그림 5 : 성능에 영향을 미치는 광선의 수

그림 6 : 성능에 영향을 미치는 광선의 길이
그림 6 : 성능에 영향을 미치는 광선의 길이

시연의 목적으로, 기본 콜라이더를 메쉬 콜라이더로 변경하기로 결정했습니다.

그림 7 : 메쉬 콜라이더 씬 (콜라이더 당 110 개의 버텍스)
그림 7 : 메쉬 콜라이더 씬 (콜라이더 당 110 개의 버텍스)

그림 8 : 기본 콜라이더 vs 메쉬 콜라이더 물리 프로파일러 데이터
그림 8 : 기본 콜라이더 vs 메쉬 콜라이더 물리 프로파일러 데이터

프로파일 그래프에서 볼 수 있듯이, 메쉬 콜라이더에 대한 레이캐스팅은 물리 엔진이 프레임당 더 많은 작업을 하도록 만듭니다.

Physics 2D vs 3D

어떤 물리 엔진이 여러분의 프로젝트에 최고 적절한지 선택하십시오. 만약 2D 나 2.5D 게임(2D 평면에서 만드는 3D 게임)을 개발 중이라면, 3D 물리 엔진을 사용하는 것은 과한 선택입니다. 이 여분의 차원이 여러분의 프로젝트에서 불필요한 CPU 비용을 차지하게 됩니다. 두 물리 엔진의 성능 차이를 확인하고 싶다면, 저자가 이전에 작성한 글을 참고해 보세요.

http://x-team.com/2013/11/unity3d-v4-3-2d-vs-3d-physics/

RigidBody

RigidBody 컴포넌트는 오브젝트 간의 물리적 상호작용을 추가하기 위한 필수 컴포넌트 입니다. 콜라이더를 트리거로써만 사용하는 경우에도, OnTrigger 이벤트를 적절히 사용하기 위해서 게임 오브젝트에 RigidBody를 추가해야 합니다. RigidBody를 가지고 있지 않은 게임 오브젝트는 정적 콜라이더로 간주됩니다. 정적 콜라이더를 움직이려는 시도를 하게 되면, 물리 엔진이 물리 세계 전체를 다시 계산하도록 강제하기 때문에 극도로 비효율적으로 동작하게 됩니다. 다행히도, 프로파일러는 여러분이 이러한 시도를 할 때 CPU 프로파일러 탭에서 경고를 보여주기 때문에 이러한 문제를 알아차릴 수 있습니다. 정적 콜라이더를 움직일 때의 영향을 좀 더 확실히 시연해 보기 위해, 첫번째 데모의 움직이는 모든 오브젝트에서 RigidBody를 제거했고, 이에 대한 프로파일러 데이터를 캡쳐했습니다.

그림 9 : 움직이는 정적 콜라이더 경고
그림 9 : 움직이는 정적 콜라이더 경고

그림에서 볼 수 있듯이, 총 2000개의 경고가 생성되었고, 이는 하나하나가 움직이는 게임 오브젝트에 대한 것입니다. 또한 물리에 사용된 평균 CPU 량도 대략 17.6ms에서 35.85ms로 증가했습니다. 게임 오브젝트를 움직일 때 RigidBody를 붙이는 것은 필수적입니다. 만약 여러분이 게임 오브젝트를 직접적으로 조정하고 싶다면, rigidbody 속성 중에 kinematic을 마트하시면 됩니다.

Fixed TimeStep

TimeManager의 Fixed Timestep 값을 변경하는 것은, FixedUpdate()와 물리 업데이트 비율에 직접적인 영향을 미칩니다. 이 값을 변경함으로써 물리에서 정확성과 CPU 사용 시간 사이의 조절을 할 수 있습니다.

요약

이번에 다룬 주제는 설정과 구현이 쉽고, 여러분이 개발하는 대부분의 프로젝트는 물리 엔진을 사용할 것이기 때문에 성능의 차이를 가져올 수 있을 것 입니다.

이 글은 유니티 튜토리얼을 번역한 글 입니다. 원문은 여기에서 확인하실 수 있습니다.

이 글은 총 5개의 챕터로 구성되며 아래와 같습니다.

  1. [번역] 에셋번들과 리소스에 대한 가이드
  2. [번역] 에셋과 오브젝트, 그리고 직렬화
  3. [번역] RESOURCES 폴더
  4. [번역] 에셋번들 기초
  5. [번역] 에셋번들 사용 패턴




이 글은 [번역] 에셋번들과 리소스에 대한 가이드 시리즈의 5번째 챕터입니다.

이 시리즈의 이전 챕터에서는 에셋번들 기초에 대해 다뤘는데, 특히 다양한 로딩 API의 저수준 동작들에 대해 이야기했습니다. 이번 챕터에는 에셋번들을 실제로 사용하는 방법에 있어서 발생하는 문제와 해결법에 대해 이야기할 것 입니다.

4.1 로드된 에셋의 관리

메모리에 민감한 환경에서 로드된 오브젝트의 크기와 갯수를 조심스레 다루는 것은 특히 중대한 문제입니다. 유니티는 현재 씬에서 오브젝트가 제거되더라도 자동으로 해당 오브젝트를 언로드하지 않습니다. 에셋의 정리는 특정 시간에 발생하게 되고, 수동으로도 발생시킬 수 있습니다.

에셋번들 자체는 조심스럽게 다뤄져야 합니다. 로컬 저장 공간에 있는(유니티 캐쉬에 있거나AssetBundle.LoadFromFile에 의해 로드된 것) 파일의 에셋번들은 최소한의 메모리 오버헤드가 있는데, 거의 10~40 KB 를 넘지 않습니다. 이러한 숫자도 에셋번들이 많은 경우에는 문제가 될 수 있습니다.

대부분의 프로젝트에서 유저들은 컨텐츠를 다시 경험할 수 있기 때문에(특정 레벨을 다시 플레이하는 등), 에셋번들을 언제 로드하고 언로드하는지를 아는 것이 중요합니다. 만약 에셋번들이 부적절하게 언로드된다면, 메모리에서 오브젝트가 중복되는 문제를 야기시킬 수 있습니다. 부적절한 에셋번들의 언로드는 특정 환경에서 바람직하지 못한 동작을 할 수도 있는데, 예를 들면 텍스쳐가 없는 상태(missing)가 될 수도 있다는 말입니다. 왜 이러한 일이 일어나는지에 대해 알고 싶으면, 에셋과 오브젝트, 그리고 직렬화 챕터의 오브젝트 내부의 참조들 섹션을 참고해 보십시오.

에셋과 에셋번들을 관리할 때 가장 중요한 것은 AssetBundle.Unload 를 true나 false 인자를 사용해서 호출할 때의 동작의 차이를 이해해야 한다는 것 입니다.

이 API는 호출된 에셋번들의 헤더 정보를 언로드합니다. 여기에 사용되는 매개변수는 이 에셋번들의 모든 생성된 오브젝트를 같이 언로드할 것인지를 가리킵니다. true인 경우에 그 에셋번들에서 만들어진 모든 객체들 또한 즉시 언로드 됩니다 - 그 객체들이 현재 씬에서 사용되고 있더라도 말이죠.

예를 들어, 매터리얼 M이 에셋번들 AB에서 로드 되었고, 매터리얼 M이 현재 씬에서 사용되고 있다고 가정해보겠습니다.

그림 1


만약 AB.Unload(true)가 호출되면, M이 씬에서 제거되고, 파괴되며 언로드되게 됩니다. 하지만, 만약 AB.Unload(false)가 호출되면, AB의 헤더만 언로드되고 M은 여전히 씬에 남아서 동작할 것 입니다. AssetBundle.Unload(false)를 호출하는 것은 M과 AB 사이의 링크를 깨트립니다. 만약 AB가 그 후에 다시 로드되면, AB에 포함되어 있는 오브젝트의 새로운 복제본이 메모리에 올라가게 됩니다.

그림 2


만약 AB가 그 후에 다시 로드되게 된다면, 에셋번들의 헤더 정보가 새로 복제되어 다시 로드되게 됩니다. 그런데, M은 새로운 AB의 복제본이 아닌 이전 AB에서 생성된 것 입니다. 유니티는 AB의 새로운 복제본과 M 사이의 링크를 형성해주지 않습니다.

그림 3


만약 M을 다시 로드하기 위해 AB.LoadAsset()을 호출하면, 유니티는 이전에 생성한 M을 AB의 인스턴스라고 해석하지 않습니다. 따라서, 유니티는 M에 대한 새로운 복제본을 생성하게 되고, 씬에는 똑같은 2개의 M에 대한 복제본이 존재하게 되는 것 입니다.

그림 4


대부분의 프로젝트에서, 이러한 동작은 바람직하지 못 합니다. 대부분의 프로젝트에서는 AssetBundle.Unload (true)를 사용해야하고, 오브젝트가 중복되지 않을 수 있는 방법을 채택해야 합니다. 흔한 두 가지 방법은 다음과 같습니다 :

  1. 레벨 사이나 로딩 화면처럼 애플리케이션의 생명 주기 동안 일시적 에셋번들이 언로드되는 지점을 체크하는 것 입니다. 이는 간단하고 가장 흔한 옵션입니다.
  2. 각 오브젝트의 참조 개수(reference-count)를 유지하고, 에셋번들은 그 오브젝트들이 하나도 사용되지 않을 때에만 언로드시킵니다. 이는 애플리케이션이 메모리의 중복없이 각 오브젝트를 로드하고 언로드할 수 있게 해줍니다.

만약 애플리케이션이 AssetBundle.Unload (false)를 사용해야만 한다면, 각 오브젝트는 다음의 두 가지 방법으로만 언로드될 수 있습니다 :

  1. 씬과 코드에서 원치않는 오브젝트에 대한 모든 참조를 제거하십시오. 이 작업이 끝나고 나면 Resources.UnloadUnusedAssets을 호출하십시오.
  2. 씬을 additive 방식이 아닌 방식으로 로드하십시오. 이는 현재 씬의 모든 오브젝트를 파괴시키고 Resources.UnloadUnusedAssets를 자동으로 호출합니다.

만약 프로젝트에 오브젝트를 로드하고 언로드하기 위해 유저를 기다리게 할 수 있는 잘 정의된 지점이 있다면(예를 들어 게임 모드나 레벨 사이), 이러한 지점들은 가능한 많은 오브젝트를 언로드하고 새로운 오브젝트를 로드하도록 사용되어야 합니다.

이렇게 하기 위한 가장 간단한 방법은 프로젝트의 불연속적인 덩어리들을 씬에 넣고, 이 씬과 연결된 모든 참조들을 에셋번들로 빌드하는 것 입니다. 그리고나서 애플리케이션은 "로딩" 씬으로 들어가서 이전 씬에 있던 에셋번들을 모두 언로드하고, 새로운 씬의 에셋번들을 로드합니다.

이 방식이 가장 간단한 방법이지만, 일부 프로젝트는 좀 더 복잡한 에셋번들 관리가 필요합니다. 통괄적으로(universal) 쓰일 수 있는 에셋번들 디자인 패턴은 없습니다. 각 프로젝트의 데이터는 다릅니다. 에셋번들에 오브젝트를 어떻게 그룹화해서 넣을지를 결정할 때, 동시에 로드되거나 업데이트 되어야하는 오브젝트들을 에셋번들에 넣는 것이 일반적으로 최선입니다.

예를 들어, RPG(role-playing game)를 생각해보자. 각 맵과 컷씬은 에셋번들로 그룹화될 수 있는데, 일부 오브젝트는 대부분의 씬에서 필요할 것 입니다. 에셋번들은 초상화, 인게임 UI, 다른 캐릭터 모델과 텍스쳐를 제공하기 위해 만들어 질 수 있습니다. 이런 오브젝트과 에셋들은 에셋번들의 두번 째 셋으로 그룹화될 수 있고, 이는 시작 시에 로드되고 앱의 생명주기 동안 로드된 채 남아 있습니다.

또 다른 문제는 에셋번들이 언로드 된 후에 다시 그 에셋번들로부터 오브젝트를 다시 로드하려고 하는 경우에 발생할 수 있습니다. 이러한 경우에, 다시 로드하는 것은 실패하게 되고 그 오브젝트는 유니티 에디터의 hierarchy에서 (Missing) 오브젝트 상태로 보이게 됩니다.

이는 주로 유니티가 그래픽 컨텍스트에 대한 제어를 잃었다가 다시 얻었을 때 발생하는데, 모바일 앱이 중지 되거나 유저가 PC를 잠금 상태로 변환했을 때가 이에 해당합니다. 이러한 경우에, 유니티는 텍스쳐와 셰이더를 GPU에 다시 업로드해야 합니다. 만약 이런 에셋을 위한 소스 에셋번들이 사용 불가능한 상태라면, 애플리케이션은 씬의 오브젝트를 "missing shader" 상태인 magenta 색상으로 그리게 됩니다.

4.2 배포

프로젝트의 에셋번들을 고객에게 배포하는 기본적인 2가지 방법이 있습니다 : 프로젝트와 동시에 설치하는 것 또는 설치 후에 다운로드하는 방법입니다. 에셋번들을 설치에 포함 시킬 지 아니면 설치 후에 전달할 지에 대한 결정은 프로젝트가 구동할 플랫폼의 수용성(capabilities)과 제한(restrictions)에 따르게 됩니다. 모바일 프로젝트는 주로 설치 후에 다운로드 하는 방식을 선택하는데, 이는 초기 설치 크기를 줄이기 위함과 무선 다운로드 크기 제한1을 맞추기 위함입니다. 콘솔과 PC 프로젝트는 일반적으로 에셋번들을 초기 설치에 포함시킵니다.

적절한 아키텍쳐는 초기에 에셋번들을 어떤 형태로 전달했는지에 상관없이 설치 후에 새로운 컨텐츠나 수정된 컨텐츠를 패치할 수 있도록 해줍니다. 여기에 대한 더 자세한 내용을 알고 싶으면, 이 글의 에셋번들로 패치하기 섹션을 살펴보세요.

4.2.1. 프로젝트와 같이 전달

에셋번들을 프로젝트에 같이 담아서 전달하는 것이 가장 간단한 배포 방법입니다. 왜냐하면 어떤 추가적인 다운로드 관리 코드가 필요없기 때문이죠. 설치 시에 왜 에셋번들을 포함시켜야 하는 지에 대한 2가지 주요한 이유는 다음과 같습니다 :

  • 프로젝트의 빌드 시간을 줄이고 더 간단한 반복 개발을 가능하게 하기 위함입니다. 만약 에셋번들이 애플리케이션으로 부터 독립적으로 업데이트 될 필요가 없다면, 그 에셋번들은 Streaming Assets으로 저장함으로써 애플리케이션에 포함될 수 있습니다. 아래의 Streaming Assets 섹션을 보세요.
  • 업데이트 가능한 컨텐츠의 초기 버전을 같이 전달하기 위함입니다. 이는 최초 설치 후 유저의 시간을 절약하기 위해서, 또는 추후의 패치를 위한 기본을 제공하기 위해서 주로 행해집니다. Streaming Assets은 이 경우에는 이상적인 방법이 아닙니다. 하지만, 커스텀 다운로드 코드와 캐시 시스템을 작성하는 것이 선택 사항이 아니라면, 업데이트 가능한 컨텐츠의 최초 버전은 Streaming Assets로부터 유니티 캐쉬에 로드될 수 있습니다.


4.2.1.1. Streaming Assets

설치 시에 유니티 애플리케이션에 어떤 형태의 컨텐츠를 포함시키는 가장 쉬운 방법은 해당 컨텐츠를 빌드 전에 /Assets/StreamingAssets/ 폴더에 넣는 것 입니다. 빌드 시에 StreamingAssets 폴더에 포함되어 있는 모든 것들은 최종 애플리케이션에 복사되어 질 것 입니다. 이 폴더는 최종 애플리케이션에 에셋번들 뿐만 아니라 어떤 종류의 컨텐츠도 저장해서 쓸 수 있습니다.

로컬 저장 공간에 있는 StreamingAssets 폴더의 전체 경로는 런타임에 Application.streamingAssetsPath 속성을 이용해서 접근할 수 있습니다. 그렇게해서 대부분의 플랫폼에서 AssetBundle.LoadFromFile을 통해 에셋번들을 로드할 수 있습니다.

안드로이드 개발자 : 안드로이드에서는, 에셋번들이 압축되어 있더라도, Application.streamingAssetsPath는 압축된 .jar 파일을 가리키게 됩니다. 이런 경우에, 각 에셋번들을 로드하기 위해 WWW.LoadFromCacheOrDownload가 사용되어야 합니다. .jar 파일의 압축을 푼 뒤에 로컬 저장소에 있는 읽기 가능한 위치로 에셋번들을 추출하는 커스텀 코드를 작성하는 것 또한 가능합니다.

메모 : Streaming Assets은 일부 플랫폼에서 쓰기 가능한 위치가 아닙니다. 만약 프로젝트의 에셋번들이 설치 후에 업데이트 되어야 한다면, WWW.LoadFromCacheOrDownload를 쓰거나 커스텀 다운로더를 작성해야 합니다. 더 자세한 내용을 알고 싶다면 커스텀 다운로더 - 저장공간 섹션을 읽어 주세요.

4.2.2. 설치 후 다운로드(Downloaded post-install)

모바일 장치에 에셋번들을 전달할 때 많이 사용되는 방법은 앱 설치 후 번들을 다운로드하는 방법입니다. 이는 또한 설치 후에 전체 애플리케이션을 다시 다운로드할 필요 없이, 새로운 컨텐츠나 정제된 컨텐츠를 업데이트할 수 있도록 해줍니다. 모바일 플랫폼에서는, 애플리케이션 이진파일은 비싸고 긴 재인증 프로세스를 거쳐야만 합니다. 따라서, 설치 후 다운로드를 위한 좋은 시스템을 개발하는 것은 매우 중요합니다.

에셋번들을 전달하는 가장 간단한 방법은 웹서버에 그 번들을 두고 WWW.LoadFromCacheOrDownloadUnityWebRequest를 통해서 전달하는 것 입니다. 유니티는 다운로드된 에셋번들을 로컬 저장 공간에 자동으로 저장합니다. 만약 다운로드한 에셋번들이 LZMA로 압축되어 있다면, 추후에 빠르게 로딩하기 위해 압축을 해제한 상태로 저장되게 됩니다. 만약 다운로드한 번들이 LZ4로 압축되어 있다면, 압축된 상태 그대로 저장되게 됩니다.

캐쉬가 가득차게 되면, 유니티는 가장 오래 전에 사용된 에셋번들을 캐쉬에서 지우게 됩니다. 더 자세한 내용은 내장 캐쉬 섹션에서 보세요.

WWW.LoadFromCacheOrDownload에는 결함이 있다는 것을 알아두세요. 에셋번들의 로드 섹션에 설명했듯이, WWW 오브젝트는 에셋번들을 다운로드하는 동안 동일한 크기의 메모리를 소비하게 됩니다. 이는 수용할 수 없는 메모리의 급증을 유발할 수 있습니다. 이를 피하기 위한 3가지 방법은 다음과 같습니다 :

  • 에셋번들의 크기를 작게 유지하세요. 에셋번들의 최대 크기는 번들이 다운로드될 때 프로젝트의 메모리 예산에 맞게 결정될 것 입니다. "downloading" 화면이 있는 애플리케이션은 백그라운드에서 에셋번들을 스트리밍하는 애플리케이션보다 보통은 더 많은 메모리를 할당합니다.
  • 유니티 5.3 이상에서는, UnityWebRequest API의 DownloadHandlerAssetBundle을 쓰도록 변경하세요. 이는 다운로드 중에 메모리의 급증을 야기하지 않습니다.
  • 커스텀 다운로더를 작성하세요. 더 자세한 정보를 원한다면, 커스텀 다운로더 섹션을 보세요.

가능하다면 UnityWebRequest를 써서 시작하는 것을 일반적으로 추천드리지만, 유니티 5.2 이하에서는 WWW.LoadFromCacheOrDownload를 추천드립니다. 유니티 기본 시스템 API의 메모리 소비, 캐쉬 동작, 성능이 특정 프로젝트에서 수용할 수 없을 정도이거나, 어떤 요구사항을 만족하기 위해 플랫폼 종속적인 코드를 실행시켜야만 하는 프로젝트의 경우에만 커스텀 다운로드 시스템에 투자를 하세요.

UnityWebRequest나 WWW.LoadFromCacheOrDownload의 사용을 막을 수도 있는 상황에 대한 예 입니다 :

  • 에셋번들 캐쉬에 대한 아주 세밀한 조절이 필요한 경우
  • 프로젝트에서 커스텀 압축 전략을 구현해야하는 경우
  • 비활성화 상태에서 데이터를 스트리밍하는 등의 특별한 요구사항을 만족하기 위해 플랫폼 제한적인 코드를 사용해야하는 경우
    • 예 : 백그라운드 상태일 때 데이터를 다운로드하기 위해 iOS의 Background Tasks API를 사용하는 경우 PC처럼 유니티의 적절한 SSL 지원이 없는 플랫폼에 SSL로 에셋번들을 전송해야하는 경우


4.2.3. 내장 캐쉬

유니티는 내장 에셋번들 캐쉬 시스템이 있는데, 이 시스템은 WWW.LoadFromCacheOrDownloadUnityWebRequest API를 통해 다운로드 된 에셋번들을 캐쉬하는데에 사용될 수 있습니다.

두 API는 모두 매개변수로 에셋번들의 버전 번호를 받는 오버로드 메서드를 가지고 있습니다. 이 번호는 에셋번들의 내부에 저장되어 있지 않고, 에셋번들 시스템에 의해 생성되지도 않습니다.

캐쉬 시스템은 WWW.LoadFromCacheOrDownloadUnityWebRequest로 전달된 가장 최근의 버전 번호를 유지하고 있습니다. 둘 중 어떤 API가 버전 번호를 넘겨받아서 호출되면, 캐쉬 시스템은 캐쉬된 에셋번들이 있는지 체크합니다. 만약 있다면, 에셋번들이 처음 캐쉬된 경우에 전달된 버전 번호와 현재 호출에 사용된 버전 번호를 비교해 봅니다. 이 번호가 같다면, 시스템은 캐쉬된 에셋번들을 로드하게 됩니다. 만약 번호가 다르다면, 캐쉬된 에셋번들이 없음을 의미하고, 유니티는 새로운 복사본을 다운로드하게 됩니다. 이 새로운 복사본은 새로운 버전 번호와 관계가 맺어집니다.

캐쉬 시스템에 있는 에셋번들은 파일명으로만 식별되고, 다운로드되는 전체 url과는 상관없습니다. 이는 같은 파일명의 에셋번들이 여러 다른 곳에 저장되어도 괜찮다는 의미입니다. 예를 들어, 하나의 에셋번들이 한 Content Delivery Network(CDN)의 여러 서버에 위치할 수 있습니다. 파일명이 같은 한, 캐쉬 시스템은 이들을 같은 에셋번들로 취급합니다.

에셋번들에 버전 번호를 할당하는 적절한 전략을 결정하고, 이 버전 번호를 WWW.LoadFromCacheOrDownload에 전달하는 것은 개별 애플리케이션에 달려있는 부분입니다. 대부분의 애플리케이션은 유니티 5의 AssetBundleManifest API를 사용할 수 있습니다. 이 API는 에셋번들의 컨텐츠에 대한 MD5 해쉬를 계산해서 각 에셋번들의 버전 번호를 생성할 수 있습니다. 에셋번들이 바뀔 때마다, 해쉬 값 또한 변하게 되고, 이는 에셋번들을 다운로드해야 함을 가리킵니다.

메모 : 유니티 내장 캐쉬 구현의 변덕(quirk)2 때문에, 오래된 에셋번들은 캐쉬가 가득 찰 때까지는 지워지지 않습니다. 유니티는 추후 릴리즈에서 이런 변덕에 대해 언급할 예정입니다.

더 자세한 내용을 알고 싶으면 에셋번들로 패치하기 섹션을 보십시오.

유니티의 내장 캐쉬는 캐쉬 객체에 있는 API를 호출함으로써 조절할 수 있습니다. 유니티 캐쉬의 동작은 Caching.expirationDelayCaching.maximumAvailableDiskSpace를 변경함으로써 조절할 수 있습니다.

Caching.expirationDelay는 에셋번들이 자동으로 삭제되기 위해 필요한 최소한의 시간(초 단위)입니다. 만약 에셋번들이 이 시간 동안 접근되지 않았다면, 자동으로 삭제될 것 입니다.

Caching.maximumAvailableDiskSpace 은 expirationDelay보다 덜 최근에 사용된 에셋번들을 삭제하기 시작하기 전에 캐쉬가 사용할 수 있는 로컬 저장 공간의 크기를 결정합니다. 여기에는 바이트 단위를 사용합니다. 한계 용량에 도달하게 되면, 유니티는 캐쉬 중에서 최근에 가장 적게 열었던 에셋번들(또는 Caching.MarkAsUsed를 통해 사용됐다고 표시된 에셋번들)을 삭제하게 됩니다. 유니티는 새로운 다운로드를 완료하기에 충분한 용량이 되기 전까지 캐쉬된 에셋번들을 삭제할 것 입니다.

메모 : 유니티 5.3에서, 내장 유니티 캐쉬를 조절하는 것은 매우 힘든 일 입니다. 캐쉬에서 특정 에셋번들을 삭제하는 것은 불가능합니다. 삭제는 시간적인 만기에 의해서, 디스크 사용 공간의 초과에 의해서, 또는 Caching.CleanCache(Caching.CleanCache 는 현재 캐쉬에 있는 모든 에셋번들을 삭제할 것 입니다.)에 의해서 발생합니다. 이는 개발 중이거나 라이브 동작 중일 때 문제가 될 수 있는데, 유니티가 애플리케이션에서 더 이상 사용되지 않는 에셋번들을 자동으로 지워주지 않기 때문입니다.

4.2.3.1. Cache Priming

에셋번들은 파일명으로 식별되기 때문에, 애플리케이션에 같이 포함되는 에셋번들의 캐쉬를 가장 주요한 것으로 설정하는 것이 가능합니다. 이를 위해서, /Assets/StreamingAssets/에 있는 각 에셋번들의 최초 버전을 저장하십시오. 이 과정은 프로젝트와 같이 전달 섹션에 있는 상세 내용과 같습니다.

애플리케이션이 최초에 실행될 때 Application.streamingAssetsPath로부터 에셋번들을 로딩함으로써 캐쉬를 로드할 수 있습니다. 그 때 부터, 애플리케이션은 WWW.LoadFromCacheOrDownloadUnityWebRequest를 일반적으로 호출할 수 있습니다.

4.2.4. 커스텀 다운로더

커스텀 다운로더를 작성하는 것은 어떻게 에셋번들이 다운로드되고, 압축 해제되고 저장되는 지에 대한 모든 제어를 애플리케이션에 줄 수 있습니다. 커스텀 다운로더를 작성하는 것은 야심찬 애플리케이션을 만드는 큰 팀에 한해서만 추천드립니다. 커스텀 다운로더를 작성할 때 생각해야 할 4가지 주요 문제에 대해 이야기 해보겠습니다 :

  • 에셋번들을 어떻게 다운로드할 것인가?
  • 에셋번들을 어디에 저장할 것인가?
  • 에셋번들을 어떻게 압축할 것인가?
  • 에셋번들을 어떻게 패치할 것인가?

에셋번들의 패치에 대한 정보를 원하시면, 에셋번들로 패치하기 섹션을 살펴보십시오.

4.2.4.1 다운로드

대부분의 애플리케이션에서, HTTP가 에셋번들을 다운로드하기에 가장 간단한 방법입니다. 하지만, HTTP 기반의 다운로더를 구현하는 것은 간단한 일이 아닙니다. 커스텀 다운로더는 메모리 초과 할당, 쓰레드 초과 사용, 쓰레드 초과 깨움(excessive thread wakeups)을 피해야만 합니다. 유니티의 WWW 클래스는 여기에서 철저히 설명하는 이유에 의해 부적절 합니다. WWW의 높은 메모리 비용 때문에, 유니티의 애플리케이션에서 WWW.LoadFromCacheOrDownload를 사용하지 않는다면 WWW 클래스는 피해야 합니다.

커스텀 다운로더를 작성할 때 3개의 옵션이 있습니다 :


4.2.4.1.1. C# 클래스

애플리케이션이 HTTPS/SSL 지원을 필요로 하지 않는다면, C#의 WebClient 클래스는 에셋번들을 다운로드하는데게 가장 간단한 메커니즘을 제공합니다. 초과 메모리 할당 없이, 비동기적으로 어떤 파일을 다운로드해서 로컬 저장 공간에 바로 넣는 것이 가능합니다.

WebClient를 이용해서 에셋번들을 다운로드하려면, 클래스의 인스턴스를 할당하고, 에셋번들을 다운로드하기 위한 URL과 목적지 경로를 전달해야 합니다. 요청의 매개변수를 통한 더 많은 제어가 필요한 경우에는 C#의 HttpWebRequest를 사용해서 다운로더를 작성하는 것이 가능합니다 :

  1. HttpWebResponse.GetResponseStream으로부터 바이트 스트림을 얻습니다.
  2. 스택에 고정 크기 바이트 버퍼를 할당합니다.
  3. 응답 스트림에서 읽어서 버퍼에 넣습니다.
  4. C#의 File.IO API나 어떤 다른 스트리밍 IO 스시템을 이용해서 버퍼를 디스크에 기록합니다.

플랫폼 메모 : iOS, 안드로이드, 윈도우즈 폰의 경우에만 유니티의 C# 런타임이 C# HTTP 클래스에 대한 HTTPS/SSL 지원을 합니다. PC에서는 C# 클래스를 통해서 HTTPS 서버에 접근하려고 하면 인증서 검증 에러(certificate validation errors)가 발생합니다.

4.2.4.1.2. 에셋 스토어 패키지

일부 에셋 스토어 패키지는 HTTP, HTTPS와 다른 프로토콜을 통해서 파일을 다운로드하도록 네이티브 코드 구현을 제공하고 있습니다. 유니티를 위한 커스텀 네이티브 코드 플러그인을 제작하기 전에 사용 가능한 에셋 스토어 패키지를 평가해보는 것을 추천드립니다.

4.2.4.1.3. 커스텀 네이티브 플러그인

커스텀 네이티브 플러그인을 작성하는 것은 유니티에서 데이터를 다운로드하기 위한 가장 시간이 많이 들고 가장 유연한 방법입니다. 높은 프로그래밍 시간과 높은 기술 위험도가 있기 때문에, 이 방법은 애플리케이션의 요구 사항을 만족할 다른 방법이 없는 경우에만 추천됩니다. 예를 들어, 유니티의 C# SSL 지원이 없는 윈도우즈, OSX, 리눅스 같은 플랫폼에서 애플리케이션이 SSL 통신을 해야만 하는 경우에, 커스텀 네이티브 플러그인이 필요할 수도 있습니다.

커스텀 네이티브 플러그인은 일반적으로 타겟 플랫폼의 네이티브 다운로드 API를 감싸게 됩니다. 예로써 iOS의 NSURLConnection과 안드로이드의 java.net.HttpURLConnection이 있습니다. 이런 API를 사용하기 위한 더 상세한 내용은 각 플랫폼의 네이티브 문서의 도움을 받으십시오.

4.2.4.2. 저장 공간

모든 플랫폼에서 Application.persistentDataPath는 애플리케이션이 실행되는 동안 유지되어야 하는 데이터를 저장하는데에 사용되어야 하는 쓰기 가능한 위치를 가리킵니다. 커스텀 다운로더를 작성할 때, 다운로드된 데이터를 저장하기 위해 Application.persistentDataPath의 하위 디렉토리를 사용할 것을 강력히 추천드립니다.

Application.streamingAssetPath는 쓰기 불가능하고 에셋번들 캐쉬로써는 나쁜 선택입니다. streamingAssetsPath의 위치에 대한 예는 다음과 같습니다 :

  • OSX : .app 패키지 안 ; 쓰기 불가능
  • Windows : 설치 디렉토리 안(예를 들어 Program Files) ; 일반적으로 쓰기 불가능
  • iOS : .ipa 패키지 안 ; 쓰기 불가능
  • 안드로이드 : 압축된 .jar 파일 ; 쓰기 불가능


4.3. 에셋 할당 전략

프로젝트의 에셋을 에셋번들로 어떻게 나눌 것인가에 대한 결정을 하는 것은 간단한 일이 아닙니다. 여기에서 지나치게 단순한 전략을 채용하는 유혹에 빠지기 쉬운데요, 예를 들면 모든 오브젝트를 하나의 에셋번들에 담는 것 처럼 말이죠. 그런데 이러한 해결책은 심각한 결함이 존재합니다 :

  • 너무 적은 에셋번들을 가지는 것은...
    • 런타임 메모리 사용량을 늘립니다.
    • 로딩 시간을 늘립니다.
    • 더 많은 다운로드를 필요로 합니다.
  • 너무 많은 에셋번들을 가지는 것은...
    • 빌드 시간을 늘립니다.
    • 개발이 복잡해 질 수 있습니다.
    • 전체 다운로드 시간이 늘어납니다.

중요 결정은 어떻게 에셋번들에 오브젝트를 그룹핑 하는가 입니다. 주요한 전략은 다음과 같습니다 :

  • 논리적인 실체(Logical entities)
  • 오브젝트 타입(Object Types)
  • 동시 컨텐츠(Concurrent content)

하나의 프로젝트에서 다른 카테고리의 컨텐츠를 위해 이러한 전략들을 섞어 쓸 수 있다는 것을 알아 두십시오. 예를 들어, 다른 플랫폼에 사용될 UI 요소들을 에셋번들에 같이 그룹핑하고, 인터렉티브 컨텐츠는 레벨이나 씬에 따라 그룹핑할 수 있습니다. 어떤 전략을 채용했는지와 상관없이, 따를 수 있는 가이드 라인이 여기에 있습니다 :

  • 거의 변화가 없는 오브젝트들과 자주 업데이트 되는 오브젝트들을 다른 에셋번들로 나누세요.
  • 동시에 로드될 것 같은 오브젝트들을 같이 그룹핑하세요.

예 : 모델과 여기에 사용되는 애니메이션과 텍스쳐

  • 만약 어떤 오브젝트가 여러 개의 다른 에셋번들에 있는 오브젝트들과 의존 관계에 있다면, 그 에셋을 독립된 에셋번들로 옮기세요.
    • 이상적으로, 자식 오브젝트를 부모 오브젝트와 같이 그룹핑하세요.
  • 만약 HD와 SD 버전의 텍스쳐처럼, 두 오브젝트가 동시에 로드될 것 같지 않다면, 이들을 분리된 에셋번들로 나누세요.
  • 만약 오브젝트가 같은 오브젝트인데 다른 임포터 셋팅을 하거나 데이터를 다르게 해서 다른 버전을 가지고 있는 경우라면, AssetBundle Variants를 대신 사용하는 것을 고려해보세요.

일단 위의 가이드라인을 지키고 나면, 특정 시점에 에셋번들의 컨텐츠의 50% 미만이 사용된다면 이 에셋번들을 나누는 것을 고려해 보세요. 또한 동시에 로드되는 작은 에셋번들(5~10개 미만)을 결합하는 것도 고려해 보세요.

4.3.1. 논리적인 실체로 그룹핑(Logical entity grouping)

논리적 실체로 그룹핑하는 방법은 프로젝트의 기능적인 부분에 기반하여 오브젝트를 그룹핑하는 전략입니다. 이 전략을 채용하면, 애플리케이션의 다른 부분은 다른 에셋번들로 분리되게 됩니다.

예 :

  • 하나의 UI 화면에 있는 텍스쳐와 레이아웃 데이터를 모두 같이 묶으세요.
  • 한 셋트의 캐릭터에 사용되는 텍스쳐, 모델, 애니메이션을 모두 같이 묶으세요.
  • 여러 레벨에 거쳐서 공유되는 텍스쳐와 모델을 모두 같이 묶으세요.

논리적 실체 그룹핑은 가장 흔한 에셋번들 전략이고, 이는 특히 다음과 같은 경우에 적합합니다 :

  • DLC3
  • 애플리케이션의 생명주기 내내 많은 곳에서 등장하는 실체들

예 :

  • 공통 캐릭터나 기본 UI 요소들
  • 플랫폼이나 성능 설정에 기반해서 단독으로 변하는 실체들

논리적 실체에 의해 에셋을 그룹핑하는 것의 장점은 변하지 않은 컨텐츠를 재 다운로드할 필요없이 쉽게 개별 실체를 업데이트할 수 있다는 것 입니다. 이러한 이유 때문에 이 전략이 특히 DLC에 맞다는 것 입니다. 이 전략은 또한 가장 메모리 효율적인 경향도 있는데, 애플리케이션이 현재 사용하는 실체에 대한 에셋번들만을 로드하기 때문입니다.

하지만, 이는 구현하기에 가장 다루기 어려운 전략입니다. 왜냐하면 에셋번들에 오브젝트를 할당하는 개발자가 정확히 어떻게, 그리고 언제 각 개별 오브젝트가 프로젝트에서 사용되는지에 익숙해야하만 하기 때문입니다.

4.3.2. 타입 그룹핑(Type Grouping)

타입 그룹핑은 가장 간단한 전략입니다. 이 전략을 쓰면, 비슷하거나 같은 타입의 오브젝트들은 같은 에셋번들에 위치하게 됩니다. 예를 들어, 다른 오디오 트랙을 에셋번들에 넣거나, 다른 언어 파일을 에셋번들에 넣는 식으로 말이죠.

이 전략이 간단하기는 하지만, 빌드 시간, 로드 시간, 업데이트라는 측면에서는 종종 가장 비효율적 입니다. 이는 로컬라이지이션 파일같이 작고 동시에 업데이트되는 파일에 가장 자주 쓰입니다.

4.3.3. 동시 컨텐츠 그룹핑(Concurrent content grouping)

동시 컨텐츠 그룹핑은 동시에 로드되고 사용되는 컨텐츠를 하나의 에셋번들에 넣는 전략입니다. 이 전략은 프로젝트의 컨텐츠가 매우 지역적인 경우(컨텐츠가 특정 위치나 시간 외에 거의 나타나지 않는 경우)에 가장 일반적으로 사용됩니다. 예로는 각 레벨마다 유일한 아트, 캐릭터, 사운드 이펙트를 사용하는 레벨 기반의 게임이 있을 수 있겠네요.

동시 컨텐츠 그룹핑을 수행하는 가장 흔한 방법은 씬에 기반한 에셋번들을 만드는 것인데, 각 씬에 기반한 에셋번들은 씬의 전체 또는 대부분의 의존성을 포함하고 있습니다.

컨텐츠가 강하게 지역적이지 않은 프로젝트에서, 그리고 애플리케이션의 생명주기 동안 컨텐츠가 나오는 위치가 바뀌는 경우에는 동시 컨텐츠 그룹핑은 논리적 실체 그룹핑으로 수렴하게 됩니다. 둘 다 에셋번들의 컨텐츠의 효율을 최대화하는데에 필수적인 전략입니다.

이러한 시나리오의 예로는 오픈 월드 게임이 있을 수 있습니다. 오픈 월드 게임에서 캐릭터는 랜덤하게 생성되고 월드 공간에 분포됩니다. 이러한 경우에, 어떤 캐릭터들이 동시에 나타날지 예측하기 쉽지 않고, 따라서 캐릭터들은 일반적으로 다른 전략을 이용해서 그룹핑 되어야 합니다.

4.4. 에셋번들로 패치하기

에셋번들로 패칭하는 것은 새로운 에셋번들을 다운로드하고 기존 것을 바꿔치는 것 정도로 간단합니다. 만약 애플리케이션의 캐쉬된 에셋번들을 관리하기 위해 WWW.LoadFromCacheOrDownloadUnityWebRequest 를 사용한다면, 이 API에 다른 버전 인자를 넘기면 됩니다. (자세한 내용을 알고 싶으시면 위의 링크를 보세요.)

패치 시스템에서 풀어야 할 더 어려운 문제는 어떤 에셋번들을 변경해야하는지 파악하는 것 입니다. 패치 시스템에는 두 정보 리스트가 필요합니다 :

  • 현재 다운로드된 에셋번들의 리스트와 이들의 버전 정보
  • 서버에 있는 에셋번들의 리스트와 이들의 버전 정보

패쳐(patcher)는 서버에 있는 에셋번들의 리스트를 다운로드하고 이미 있는 에셋번들 리스트와 비교해야 합니다. 없는 에셋번들이나, 버전 정보가 바뀐 에셋번들은 다시 다운로드되어야 합니다.

유니티 5의 에셋번들 시스템은 빌드가 끝나면 하나의 추가적인 에셋번들을 생성합니다. 이 여분의 에셋번들은 AssetBundleManifest 오브젝트를 포함하고 있습니다. 이 매니페스트 오브젝트는 에셋번들의 리스트와 그들의 해쉬 값을 포함하고 있고, 클라이언트에 가능한 에셋번들의 리스트와 버전 정보를 전달하는 데에 사용될 수 있습니다. 에셋번들 매니페스트 번들에 대한 더 자세한 내용을 알고 싶으시면 유니티 매뉴얼을 참고하세요.

에셋번들의 변화를 탐지하기 위한 커스텀 시스템을 작성하는 것도 가능합니다. 커스텀 시스템을 만드는 대부분의 개발자는 에셋번들 파일 리스트를 위한 산업 표준(industry-standard) 데이터 포맷을 사용해야하는데, 여기에는 JSON같은 것이 있고, 체크섬을 계산하기 위해 표준 C# 클래스를 사용하는데 예를 들면 MD5가 있습니다.

4.4.1. 차이 패치 (Differential patching)

유니티 5에서, 유니티는 결정론적인 방법으로 데이터가 정렬된 에셋번들을 빌드할 수 있습니다. 이는 애플리케이션에의 커스텀 다운로더에서 차이 패치를 구현 가능케 합니다. 에셋번들을 결정론적인 레이아웃으로 빌드하기 위해, BuildAssetBundles API를 호출할 때, BuildAssetBundleOptions.DeterministicAssetBundle 플래그를 전달하십시오.

유니티는 차이 패치를 위한 내장된 매커니즘을 제공하지 않고 있고, WWW.LoadFromCacheOrDownloadUnityWebRequest 중 어떤 것도 내장 캐시 시스템을 쓸 때 차이 패치를 수행하지 않습니다. 만약 차이 패치가 필요하다면, 커스텀 다운로더를 작성해야 합니다.

4.4.2. iOS On-Demand Resources

On-Demand Resources 는 컨텐츠를 iOS와 TVOS 장치에 제공하기 위한 애플 API입니다. 이는 iOS 9 장치에서 쓸 수 있습니다. 이 기능은 앱스토어에 런치하기 위해 현재 요구되는 조건은 아니지만, TVOS 앱에서는 런치에 필요한 요구조건 입니다.

애플의 On-Demand Resources 시스템에 대한 일반적인 아웃라인은 애플 개발자 사이트에서 찾아 볼 수 있습니다.

유니티 5.2.1 에서, App Slicing과 On-Demand Resources 둘 다에 대한 지원은 Asset Catalogs라는 다른 애플 시스템에 만들어져 있습니다. 유니티 에디터에서 콜백을 등록한 뒤에 iOS 애플리케이션을 위한 빌드 파이프라인은 Asset Catalogs에 자동으로 위치할 파일들을 보고하고 특정 On-Demand Resources 태그를 할당합니다.

새로운 UnityEngine.iOS.OnDemandResources API는 런타임에 On-Demand Resources 파일을 가져오고 캐쉬할 수 있는 기능을 제공합니다. 일단 자원을 ODR을 통해 가져왔다면, 이 자원은 AssetBundle.LoadFromFile API를 이용해서 유니티에 로드될 수 있습니다.

더 자세한 내용과 예제 프로젝트를 보고 싶다면 이 유니티 포럼의 글을 참고 하세요.

4.5. 일반적인 실수

이 섹션에서는 에셋번들을 사용하는 프로젝트에서 일반적으로 나타나는 몇몇 문제에 대해 다룹니다.

4.5.1. 에셋 중복

유니티 5의 에셋번들 시스템은 어떤 오브젝트가 에셋번들 안에 들어 있다면, 이 오브젝트에 대한 모든 의존 관계를 가려버립니다. 이는 Asset Database를 사용할 때 발생합니다. 이러한 의존 관계 정보는 하나의 에셋번들에 포함될 오브젝트들을 결정하는 데에 사용됩니다.

하나의 에셋번들에 명시적으로 할당된 오브젝트들은 그 에셋번들에만 빌드될 것 입니다. 오브젝트가 "명시적으로 할당되었다"라는 것은 오브젝트의 에셋 임포터가 assetBundleName 속성을 비어있지 않은 스트링으로 가지고 있을 때를 말합니다. 이는 유니티 에디터의 해당 오브젝트 인스펙터에서 에셋번들을 선택하거나 에디터 스크립트에서 선택함으로써 이루어집니다.

만약 두 개의 다른 오브젝트가 각각 다른 에셋번들에 할당되었는데, 둘다 공통의 오브젝트에 대한 참조를 가지고 있다면, 그 의존 관계의 오브젝트는 두 에셋번들에 복제되게 됩니다. 복제된 의존 관계의 오브젝트는 인스턴스화 되는데, 이는 두 개의 복제된 의존 관계 오브젝트가 다른 식별자를 가진 별개의 오브젝트로 간주됨을 의미합니다. 이는 애플리케이션의 에셋번들 총 크기를 증가시킬 것 입니다. 또한 애플리케이션에서 이 두개의 오브젝트를 로드하게 되면 두개의 다른 복제본이 메모리에 로드되게 됩니다.

이를 해결하기 위한 몇 가지 방법이 있습니다 :

  1. 다른 에셋번들에 빌드될 오브젝트들은 의존 관계를 공유하지 않도록 하십시오. 의존 관계를 공유하는 어떤 오브젝트들이 있다면, 이들은 같은 에셋번들에 넣음으로써 의존 관계의 복제를 막을 수 있습니다.
    • 이러한 방법은 일반적으로 많은 의존 관계를 공유하는 프로젝트에는 적합하지 않습니다. 이런 경우에는 단일 에셋번들이 생겨서 엄청나게 자주 재빌드되고 재 다운로드되는 일이 생길 수 있습니다.
  2. 의존 관계를 공유하고 있는 어떠한 두 개의 에셋번들도 동시에 로드될 수 없도록 에셋번들을 분할 하십시오.
    • 이러한 방법은 레벨 기반의 게임같은 타입의 프로젝트에서 잘 동작할 수 있습니다. 하지만, 이는 여전히 프로젝트의 에셋번들 크기를 불필요하게 늘리고, 빌드 시간과 로딩 시간 또한 늘립니다.
  3. 모든 의존 관계 에셋들을 그들 만의 에셋번들에 빌드하십시오. 이는 전적으로 중복된 에셋에 대한 위험 부담을 제거할 수 있지만, 복잡성을 증가시킵니다. 애플리케이션은 에셋번들 간의 의존 관계를 추적해야만 하고, 어떤 AssetBundle.LoadAsset API를 호출하기 전에 적절한 에셋번들들이 로드된 상태를 보장해야만 합니다.

유니티 5에서, UnityEditor 네임스페이스에 있는 AssetDatabase API를 통해 오브젝트 간의 의존 관계를 추적할 수 있습니다. 네임스페이스가 함축하고 있듯이, 이 API는 런타임이 아닌 유니티 에디터에서만 사용 가능합니다. AssetDatabase.GetDependencies는 특정 오브젝트나 에셋의 모든 직접적인 의존 관계를 얻는 데에 사용될 수 있습니다. 이러한 의존 관계에는 스스로에 대한 의존 관계도 포함되어 있음을 기억하십시오. 추가로, AssetImporter API는 에셋번들에 어떤 특정 오브젝트가 할당됐는지 확인하기 위해 사용될 수 있습니다.

AssetDatabase와 AssetImporter API를 조합하면, 에셋번들의 모든 직접 또는 간접 의존 관계가 어떤 에셋번들에 할당되도록 하거나, 의존 관계를 공유하는 두 개의 에셋번들이 존재하지 않도록 하는 에디터 스크립트를 작성하는 것이 가능합니다. 중복되는 에셋의 메모리 비용 때문에, 모든 프로젝트에서 그러한 스크립트를 사용하기를 추천드립니다.

4.5.2. 스프라이트 아틀라스 중복

이어지는 섹션에서는 유니티 5의 에셋 의존 관계 계산 코드가 자동 생성되는 스프라이트 아틀라스과 함께 사용될 때의 변덕에 대해 다룹니다. 유니티 5.2.2p4와 유니티 5.3에서는 이런 동작을 해결하기 위한 패치가 이루어 졌습니다.

유니티 5.2.2p4과 5.3 그리고 그 이상

자동 생성된 스프라이트 아틀라스는 자신이 생성된 스프라이트 오브젝트를 포함하고 있는 에셋번들에 할당됩니다. 만약 그 스프라이트 오브젝트가 여러 개의 에셋번들에 할당되어 있다면, 스프라이트 아틀라스는 하나의 에셋번들에 할당되지 않고 중복되게 됩니다.

스프라이트 아틀라스가 중복되지 않게 하려면, 같은 아틀라스로 태그된 모든 스프라이트가 같은 에셋번들에 할당되도록 하십시오.

유니티 5.2.2p3와 그 이하

자동 생성된 스프라이트 아틀라스는 절대 어떤 한 에셋번들에 할당되지 않습니다. 이 때문에, 이 아틀라스는 이를 구성하고 있는 스프라이트가 포함된 에셋번들과, 이 스프라이트들이 참조하고 있는 에셋번들에도 포함되어 집니다.

이러한 문제때문에, 유니티의 스프라이트 패커를 사용하는 모든 유니티 5 프로젝트는 유니티 5.2.2p4나 5.3 또는 그 이상의 새로운 버전으로 업그레이드하기를 강력하게 추천하는 바입니다.

업그레이드할 수 없는 프로젝트의 경우에는 2가지 회피 방법이 있습니다 :

  1. 쉬운 방법 : 유니티의 내장 스프라이트 패커를 사용하지 마십시오. 외부 툴을 이용해서 생성하는 스프라이트 아틀라스는 일반 에셋이 되고, 에셋번들에 적절히 할당될 수 있습니다.
  2. 어려운 방법 : 자동 아틀라스 스프라이트를 사용하는 모든 오브젝트를 스프라이트와 같은 에셋번들에 할당하십시오.

  3. 이는 생성된 스프라이트 아틀라스를 다른 에셋번들의 간접 의존 관계로 보지 않기 때문에 중복되지 않을 것 입니다.

  4. 이 해결책은 유니티 스프라이트 패커를 사용하는 단순한 워크 플로우를 보존하지만, 에셋을 다른 에셋번들로 분리시킬 수 있는 능력을 감소시키고, 아틀라스가 변하지 않더라도 아틀라스를 참조하는 어떤 컴포넌트의 데이터 변화가 발생하면 전체 스프라이트 아틀라스를 다시 다운로드해야만 합니다.


4.5.3. 안드로이드 텍스쳐

안드로이드 생태계의 심각한 디바이스 파편화 때문에, 텍스쳐를 몇몇 다른 포맷으로 압축하는 것은 종종 필수적입니다. 모든 안드로이드 장치가 ETC1을 지원하지만, ETC1은 텍스쳐의 알파 채널을 지원하지 않습니다. 애플리케이션이 OpenGL ES 2 지원을 필요로하지 않는다면, 이 문제를 해결하기 위한 가장 명확한 방법은 ETC2를 사용하는 것이고, 이는 OpenGL ES 3를 지원하는 모든 안드로이드 장치에서 사용 가능합니다.

대부분의 애플리케이션은 ETC 지원이 안되는 오래된 기종에도 설치할 필요가 있습니다. 이를 해결하기 위한 하나의 방법은 유니티 5의 AssetBundle Variants를 사용하는 것 입니다. (다른 옵션에 대한 자세한 내용을 알고 싶으면, 유니티의 안드로이드 최적화 가이드를 봐주세요.)

AssetBundle Variants를 사용하기 위해서, 확실하게 ETC1으로 압축할 수 없는 모든 텍스쳐들은 텍스쳐만 있는 에셋번들로 분리해야 합니다. 다음으로, 안드로이드 생태계에서 ETC2를 수용할 수 없는 장치를 지원하기 위해 충분한 에셋번들 variants를 만들어야 합니다. 이 때 제조사 특유의 압축 포맷을 사용할 수 있는데, DXT5, PVRTC, ATITC 등이 있습니다. 각 에셋번들 variant에 대해서, 포함된 텍스쳐의 TextureImporter 셋팅의 압축 포맷을 variant에 적절하게 변경하십시오.

런타임에, 다른 텍스쳐 압축 포맷에 대한 지원은 SystemInfo.SupportsTextureFormat API를 이용해서 탐지할 수 있습니다. 이 정보는 지원되는 포맷으로 압축된 텍스쳐를 포함하고 있는 에셋번들 Variant를 선택하고 로드하는데에 사용될 수 있습니다.

안드로이드 텍스쳐 압축 포맷에 대한 더 자세한 내용은 여기에서 보실 수 있습니다.

4.5.4. iOS 파일 핸들 남용

이 섹션에 나오는 내용은 유니티 5.3.2p2에서 수정되었습니다. 현재 유니티 버전은 이 이슈에 영향을 받지 않습니다.

유니티 5.3.2p2 이전 버전에서는, 에셋번들이 로드되어 있는 전체 시간 동안 유니티가 그 에셋번들에 대한 파일 핸들을 유지하고 있었습니다. 이는 대부분의 플랫폼에서는 문제가 되지 않습니다. 하지만, iOS에서는 한 프로세스에서 동시에 열 수 있는 파일 핸들의 수가 255개로 제한되어 있습니다. 만약 에셋번들을 로드하다가 이러한 제한을 초과하게 되면, "Too Many Open File Handles" 에러를 내뿜으로 로딩 콜이 실패하게 됩니다.

이는 컨텐츠를 수백개 또는 수천개의 에셋번들로 나누려고하는 프로젝트에서 흔히 발생할 수 있는 문제였습니다.

패치된 버전의 유니티로 업그레이드할 수 없는 프로젝트에서는, 임시 방편이 있습니다 :

  • 관련된 에셋번들을 합침으로써 사용 중인 에셋번들의 수를 줄이는 방법이 있습니다.
  • 에셋번들의 파일 핸들을 닫기 위해 AssetBundle.Unload를 사용하는 것과 로드된 오브젝트의 생명주기를 수동으로 관리하는 방법이 있습니다.


4.6. 에셋번들 Variants

유니티 5의 에셋번들 시스템의 주요 특징은 AssetBundle Variants의 도입입니다. Variants의 목적은 애플리케이션을 런타임 환경에 더 잘 맞게 컨텐츠를 조절하도록 하는 것 입니다. 오브젝트를 로딩하고 Instance ID 참조를 풀 때, Variants는 다른 에셋번들 파일에 있는 다른 UnityEngine.Object를 마치 "같은" 오브젝트 인 것 처럼 보이게 해줍니다. 개념상, 이는 두 개의 UnityEngine.Object가 같은 File GUID와 Local ID를 공유하는 것처럼 보이게 해주고, 스트링 Variant ID를 이용해 로드할 실제 UnityEngine.Object를 식별합니다.

이 시스템의 2가지 주된 사용 사례가 있습니다 :

  1. Variants는 정해진 플랫폼에 대해 적절한 에셋번들을 로딩하는 것을 간단하게 해줍니다.
    • 예 : 빌드 시스템은 DirectX 11 윈도우 빌드를 위한 높은 해상도의 텍스쳐와 복잡한 셰이더를 포함하는 에셋번들과 안드로이드를 위한 더 낮은 정밀도의 컨텐츠를 포함하는 에셋번들을 빌드할 수 있습니다. 런타임에, 프로젝트의 리소스 로딩 코드는 플랫폼에 맞는 에셋번들 Variant를 로드할 수 있고, [AssetBundle.Load] API에 넘겨질 오브젝트 이름은 동일하게 사용할 수 있습니다.
  2. Variants는 같은 플랫폼이지만 하드웨어가 다른 경우, 다른 컨텐츠를 로드할 수 있도록 해줍니다.
    • 이 것이 넓은 범위의 모바일 장치들을 지원할 수 있는 핵심입니다. iPhone 4는 iPhone 6에서 보여주는 정밀도의 컨텐츠를 보여줄 수 없습니다.
    • 안드로이드에서, 에셋번들 Variants는 장치들 간의 엄청난 화면 비율과 DPI에 대한 파편화를 해결하는데에 사용될 수 있습니다.


4.6.1. 한계

에셋번들 Variant 시스템의 핵심 한계는 구별되는 에셋으로부터 Variants를 빌드할 필요가 있다는 점 입니다. 이 한계는 에셋의 임포트 셋팅에 대한 차이만으로도 적용 가능합니다. 만약 Variant A와 Variant B의 차이가 유니티 텍스쳐 임포터에서 설정한 텍스쳐 압축 알고리즘이 유일한 차이라면, Variant A와 Variant B는 완전히 다른 에셋입니다. 이는 Variant A와 Variant B는 디스크 상에서 별도 파일로 저장되어야 함을 의미합니다.

이러한 한계점은 소스 컨트롤에 의해 유지되어야 하는 특정 에셋에 대한 여러 개의 복제본을 가지는 큰 프로젝트의 관리를 복잡하게 만듭니다. 개발자가 그 에셋의 컨텐츠를 바꾸려고 하면, 해당 에셋의 모든 복제본이 업데이트 되어야 합니다.

이 문제에 대해 내장된 회피 수단은 존재하지 않습니다.

대부분의 팀은 그 팀만의 에셋번들 Variants 형태를 구현합니다. 이는 에셋번들의 파일명 뒤에 잘 정의된 접미사를 붙임으로써 할 수 있습니다. 이 때 접미사를 붙이는 이유는 그 에셋번들에 특정 variant를 식별하기 위함입니다. 커스텀 코드는 이러한 에셋번들을 빌드할 때, 포함되는 에셋의 임포터 셋티을 프로그램적으로 변경합니다. 일부 개발자들은 프리팹에 붙어있는 컴포넌트의 인자를 변경하기 위해 자신의 커스텀 시스템을 확장하기도 합니다.

4.7. 압축 또는 비압축?

에셋번들을 압축할지 말지에 대해서는 신중한 고려가 필요합니다. 중요한 질문은 다음과 같습니다 :

  • 에셋번들의 로딩 시간이 중요한 요소인가? 로컬 저장 공간이나 로컬 캐쉬에서 로딩할 때, 비압축 에셋번들이 압축 에셋번들보다 훨씬 빠르게 로드됩니다. 원격 서버에서 압축된 에셋번들을 다운로드하는 것은 일반적으로 압축되지 않는 에셋번들을 다운로드하는 것보다 빠릅니다.
  • 에셋번들의 빌드 시간이 중요한 요소인가? LZMA와 LZ4는 파일을 압축할 때 매우 느리고, 유니티 에디터는 에셋번들을 순차적으로 처리합니다. 에셋번들의 개수가 많은 프로젝트는 이들을 압축하는데에 많은 시간이 걸립니다.
  • 애플리케이션의 크기가 중요한 요소인가? 만약 애플리케이션에 에셋번들이 포함된 상태로 전달된다면, 에셋번들을 압축하는 것이 애플리케이션의 전체 사이즈를 줄일 수 있습니다. 대안으로, 에셋번들을 설치 후에 다운로드할 수도 있습니다.
  • 메모리 사용량이 중요한 요소인가? 유니티 5.3 이전에, 모든 유니티의 압축 해제 메커니즘은 압축을 해제하기 전에 압축된 에셋번들 전체를 메모리에 로드할 필요가 있었습니다. 만약 메모리 사용량이 중요하다면, 비압축이나 LZ4로 압축된 에셋번들을 사용하세요.
  • 다운로드 시간이 중요한 요소인가? 에셋번들이 크거나, 모바일에서 3G로 다운로드하는 것처럼 대역폭이 제한적인 환경에 유저가 처해져 있는 등의 경우에 압축이 필요할 지도 모릅니다. 만약 빠른 속도로 연결되어 있는 PC에 수십 MB의 데이터를 전달하는 경우라면, 압축을 생략하는 것도 가능합니다.


4.8. 에셋번들과 WebGL

유니티는 WebGL 프로젝트에서는 압축된 에셋번들을 사용하지 말 것을 강력히 추천합니다.

유니티 5.3에서, WebGL 프로젝트에서 모든 에셋번들의 압축 해제와 로딩은 메인 쓰레드에서 발생합니다. 이는 유니티 5.3의 WebGL 익스포트 옵션에서는 워커 쓰레드를 지원하지 않기 때문입니다. ( 에셋번들의 다운로드는 브라우저로 위임되어 XMLHttpRequest 자바스크립트 API를 통해 이루어지고, 유니티의 메인 쓰레드 밖에서 발생할 것 입니다.) 이는 WebGL에서 압축된 에셋번들을 매우 비쌈을 의미합니다.

대게는 에셋번들을 압축하지 않은 상태로 빌드하고, 애플리케이션의 컨텐츠를 제공하는 웹 서버가 에셋번들을 gzip 압축 형태로 제공하도록 설정하는 것이 훨씬 빠릅니다. gzip 압축이 LZMA와 같은 압축률을 낼 수는 없지만, 에셋번들은 브라우저에 의해 워커 쓰레드에서 압축 해제 되고, 유니티 애플리케이션은 에셋번들의 압축을 해제하는 동안 오랫 동안 멈춰있는 경험을 하지 않아도 됩니다.


  1. over-the-air download size limit을 무선 다운로드 크기 제한이라 번역했는데요, 아마 iOS나 안드로이드에서 통신사 네트웍으로 연결되어 있을 때 다운로드 받을 수 있는 최대 크기 제한이 아닐까 추측합니다.

  2. quirk를 변덕이라 번역하는게 맞는 것인지 잘 모르겠네요. 문맥상으로는 결함 비슷한 것 같긴 합니다.

  3. DLC - 본문에는 설명이 없지만 DownLoadable Contents를 의미하는 것 같습니다.

이 글은 유니티 튜토리얼을 번역한 글 입니다. 원문은 여기에서 확인하실 수 있습니다.

이 글은 총 5개의 챕터로 구성되며 아래와 같습니다.

  1. [번역] 에셋번들과 리소스에 대한 가이드
  2. [번역] 에셋과 오브젝트, 그리고 직렬화
  3. [번역] RESOURCES 폴더
  4. [번역] 에셋번들 기초
  5. [번역] 에셋번들 사용 패턴




이 글은 [번역] 에셋번들과 리소스에 대한 가이드 시리즈의 4번째 챕터입니다.

이 챕터에서는 에셋번들에 대해 이야기합니다. 여기에는 에셋번들의 기초 시스템과 에셋번들과 상호작용하는데에 사용되는 핵심 API를 포함하고 있습니다. 특히, 에셋번들 자신을 로드/언로드하는 것과 에셋번들에서 특정 에셋과 오브젝트를 로드/언로드하는 것에 대해서도 이야기 합니다.

에셋번들의 사용에 대한 다른 패턴과 모범 사례를 알고 싶다면, 이 시리즈의 다음 챕터를 보시기 바랍니다.

3.1. 개요(Overview)

에셋번들 시스템은 한 개 이상의 파일을 유니티가 색인할 수 있는 압축 포맷으로 저장하는 기능을 제공합니다. 이 시스템의 목적은 유니티의 직렬화 시스템과 호환되는 데이터 전송 방법을 제공하는 것 입니다. 에셋번들은 유니티에서 설치 후에 코드가 없는 컨텐츠를 전송하고 업데이트하는 주요 도구입니다. 이로 인해서 개발자들은 최초 설치 앱의 에셋 사이즈를 줄일 수 있고, 런타임 메모리 압축을 최소화할 수 있고, 최종 유저(end-user)의 디바이스에 최적화된 컨텐츠를 선택적으로 로드할 수 있습니다.

에셋번들이 동작하는 방식을 이해하는 것은 모바일 디바이스를 위한 유니티 프로젝트를 빌드하는데 필수적인 부분입니다.

3.2. 에셋번들 안에는 무엇이 있나?

에셋번들은 두 부분으로 구성되어 있습니다 : 헤더(header)와 데이터(data) 영역(segment)으로 말이죠.

헤더는 유니티에서 에셋번들이 빌드될 때 생성됩니다. 여기에는 에셋번들에 대한 정보가 포함되어 있는데, 여기에는 에셋번들의 식별자(identifier), 압축 여부, 매니페스트 (manifest)1 등이 있습니다.

매니페스트는 오브젝트의 이름을 키(key)로 하는 색인 테이블입니다. 각 항목(entry)는 바이트 인덱스를 제공하는데, 이는 에셋번들의 데이터 영역에서 특정 오브젝트를 어디에서 찾을 수 있는지를 가리키고 있습니다. 대부분의 플랫폼에서 이 색인 테이블은 STL std::multimap 으로 구현되어 있습니다. STL의 플랫폼마다 구현에 사용되는 알고리즘은 달라지지만, 대부분은 균형 탐색 트리(balanced search tree)의 변형입니다. 윈도우즈와 OSX에서 파생된 플랫폼들(iOS를 포함해서요)에서는 red-black 트리를 사용합니다. 그러므로, 매니페스트 파일을 생성하는데에 걸리는 시간은 에셋번들 안에 있는 에셋의 수의 증가율보다 더 높습니다.

데이터 영역에는 에셋번들에 있는 에셋을 직렬화해서 생기는 미가공 데이터(raw data)2를 포함하고 있습니다. 만약 데이터 영역을 압축한다면, 모든 에셋을 직렬화한 데이터인 축적된 직렬화 바이트 시퀀스(collective sequence of seralized bytes)에 LZMA 알고리즘이 적용되고, 바이트 배열 전체가 압축되게 됩니다.

유니티 5.3 이전에는, 에셋번들 안에 오브젝트가 개별적으로 압축될 수 없었습니다. 그 결과, 유니티 버전 5.3 이전에서 압축된 에셋번들에 있는 한 개 이상의 오브젝트를 읽으려고 하면, 유니티는 그 에셋번들 전체의 압축을 풀어야만 했습니다. 일반적으로, 추후에 같은 에셋번들에 대한 로딩 요청에 대한 로딩 성능을 개선하기 위해서, 유니티는 압축 해제된 에셋번들에 대한 복사본을 캐쉬해 둡니다.

유니티 5.3에서 LZ4 압축 옵션이 추가되었습니다. LZ4 압축 옵션으로 빌드된 에셋번들은 에셋번들 내에 개별적인 오브젝트들을 압축할 것이고, 유니티는 압축된 에셋번들을 디스크에 저장하게 됩니다. 이는 유니티가 에셋번들 전체에 대한 압축을 풀 필요없이 개별 오브젝트를 압축 해제할 수 있도록 해줍니다.

3.3. 에셋 번들 관리자(The AssetBundle Manager)

유니티는 Bitbucket에 AssetBundle Manager에 대한 참조 구현을 개발했고 유지하고 있습니다. 이 관리자는 이번 챕터에서 자세히 다루는 많은 컨셉과 API를 채용하고 있으며, 에셋번들을 리소스 관리(resource-management) 작업 흐름(workflow)에 합쳐야 하는 어떠한 프로젝트에서든지 유용=할 수 있는 시작 지점을 제공하고 있습니다.

주목할만한 기능에는 "simulation mode"가 있다. 유니티 에디터가 활성되 되었을 때, 이 모드는 에셋번들에 태그된 에셋에 대한 요청을 프로젝트의 /Assets/ 폴더에 있는 원래의 에셋 위치로 투명하게(transparently, 자동으로) 방향을 틀어줍니다(redirect). 이는 개발자들이 에셋번들을 재 빌드할 필요없게 만들어 줍니다.

AssetBundle Manager는 오픈 소스이고 여기에서 볼 수 있습니다.

3.4. 에셋번들의 로드

유니티 5에서, 에셋번들은 4개의 API를 이용해서 로드될 수 있습니다. 이 4개의 API의 동작은 2개의 영역으로 구분지어 다르게 볼 수 있습니다.

  1. 에셋번들이 LZMA로 압축되었는지, LZ4로 압축되었는지, 아니면 압축되지 않았는지
  2. 에셋번들이 로드될 플랫폼


4개의 API는 다음과 같습니다.



3.4.1. AssetBundle.LoadFromMemoryAsync

유니티의 추천에서는 이 API를 사용하지 말라고 하고 있습니다.

유니티 5.3.3 업데이트 : 이 API는 유니티 5.3.3에서 이름이 변경되었습니다. 유니티 5.3.2와 그 이전에는 AssetBundle.CreateFromMemory 였고, 기능의 변화는 없습니다.

AssetBundle.LoadFromMemoryAsync 는 관리되는(managed) 코드 바이트 배열(C#의 byte[]) 에서 에셋번들을 로드합니다. 이는 항상 관리된 코드 바이트 배열로부터 소스 데이터를 복사하는데, 이 때 네이티브 메모리의 연속된 블락에 새롭게 할당을 하게 됩니다. 만약 에셋번들이 LZMA 압축이 되었다면, 복사하는 중에 에셋번들의 압축을 해제하게 됩니다. 압축되지 않았거나 LZ4로 압축이 되었다면 에셋번들은 그대로 복사되게 됩니다.

이 API를 사용할 때에 소비되는 메모리의 최대량은 최소한 에셋번들의 크기의 2배가 됩니다 : 하나는 API에 의해 생성되는 네이티브 메모리이고, 다른 하나는 API에 전달되는 관리되는 바이트 배열입니다. 따라서 이 API를 통해서 에셋번들에서 로드된 에셋들은 메모리에 3번 복제되게 됩니다 : 한번은 관리된 코드 바이트 배열, 한번은 에셋번들의 네이티브 메모리 복사본, 또 한번은 에셋 자체를 로드하기 위한 GPU나 시스템 메모리를 의미합니다.

3.4.2. AsssetBundle.LoadFromFile

유니티 5.3 업데이트 : 이 API는 유니티 5.3에서 이름이 변경되었습니다. 유니티 5.2와 그 이전에는 AssetBundle.CreateFromFile 이었고, 기능의 변화는 없습니다.

AssetBundle.LoadFromFile은 하드 디스크나 SD카드같은 로컬 저장소에서 압축되지 않은 에셋번들을 로드하는 목적의 고효율 API입니다. 만약 에셋번들이 압축되지 않았거나 LZ4 압축일 경우 이 API는 다음과 같이 동작하게 됩니다 :

모바일 디바이스 : 이 API는 에셋번들의 헤더 부분만 로드하고 나머지 데이터 부분은 디스크에 남겨둡니다. 에셋번들의 오브젝트는 로딩 메서드(AssetBundle.Load 같은)가 호출되거나 Instance ID가 역참조될 때 로드됩니다. 이 시나리오에서는 초과 메모리 사용은 없습니다.

유니티 에디터 : 이 API는 에셋번들 전체를 메모리에 로드합니다. 마치 바이트를 디스크에서 읽었고, AssetBundle.LoadFromMemoryAsync 를 사용한 것처럼 말이죠. 만약 유니티 에디터에서 해당 프로젝트를 프로파일링 해본다면, 이 API로 인해 에셋번들을 로딩하는 동안 메모리 사용량이 확 증가하는 현상을 보실 수 있습니다. 이는 디바이스 상에서 성능에 영향을 주지 않아야 하며 보강 행동(remedial action)을 취하기 전에 디바이스 상에서 다시 테스트가 진행되어야 합니다.

메모 : 유니티 5.3 이하이고 안드로이드 기기에서는 Streaming Assets 경로로부터 에셋번들을 로드하려고하면 이 API는 실패하게 됩니다. 왜냐하면 해당 경로의 컨텐츠는 압축된 .jar 파일 내에 존재하기 때문이죠. 더 자세한 내용이 궁금하다면, 에셋번들 사용 패턴 챕터배포- 프로젝트와 같이 전달 섹션을 읽어보세요. 이 이슈는 유니티 5.4에서 해결됐습니다. 유니티 5.4 이상으로 빌드된 게임은 Streaming Assets에서 에셋번들을 로드할 때 이 API를 사용할 수 있습니다.

메모 : AssetBundle.LoadFromFile 은 LZMA 압축된 에셋번들을 로드할 때 항상 실패합니다.

3.4.3. WWW.LoadFromCacheOrDownload

WWW.LoadFromCacheOrDownload 는 원격 서버나 로컬 저장 공간에서 오브젝트를 로드하기에 유용한 API입니다. 로컬 저장 공간에 있는 파일은 file:// URL을 이용해서 로드할 수 있습니다. 만약 어떤 에셋번들이 유니티 캐쉬에 있다면, 이 API는 AssetBundle.LoadFromFile 똑같이 동작하게 됩니다.

만약 에셋번들이 캐쉬되지 않았다면, WWW.LoadFromCacheOrDownload 는 에셋번들을 특정 소스에서 읽게 됩니다. 만약 에셋번들이 압축되어 있다면, 작업 쓰레드(worker thread)를 이용해서 압축 해제되고 캐쉬에 쓰여지게 됩니다. 압축되어 있지 않다면, 작업 쓰레드를 이용해서 바로 캐쉬에 쓰여지게 됩니다.

일단 에셋번들이 캐쉬되고 나면, WWW.LoadFromCacheOrDownload는 캐쉬에서 헤더 정보를 로드하고 에셋번들의 압축을 해제합니다. 그 후에는 AssetBundle.LoadFromFile을 이용해 에셋번들을 로드한 것과 똑같이 API가 동작하게 됩니다.

메모 : 데이터가 압축 해제되고 고정 크기 버퍼(fixed-size buffer)를 이용해 캐쉬되는 동안, WWW 오브젝트는 해당 에셋번들의 바이트에 대한 전체 복사본을 네이티브 메모리에 유지합니다. 이러한 에셋번들의 추가 복사는 WWW.bytes 속성을 지원하기 위해 유지되는 것 입니다.

WWW 오브젝트에서 에셋번들의 바이트를 캐슁하는 것에 대한 메모리 오버헤드 때문에, WWW.LoadFromCacheOrDownload를 사용하는 모든 개발자들은 에셋번들의 크기를 작게 만들도록 추천합니다 - 최대 몇 메가 바이트 이내로 말이죠. 모바일 기기처럼 제한된 메모리를 가진 플랫폼에서 동작시켜야하는 개발자들에게는, 메모리가 갑자기 증가하는 것을 피하기 위해 한 번에 단 하나의 에셋번들만 다운로드하도록 추천합니다. 에셋번들의 크기에 대해서 더 알고 싶다면 에셋번들 사용 패턴 챕터의 에셋 할당 전략 섹션을 참고해 주세요.

메모 : 이 API를 호출할 때마다 새로운 작업 쓰레드가 생성되게 됩니다. 이 API를 여러번 호출할 때 지나친 수의 쓰레드가 생성되지 않게 조심하십시오. 만약 5~10개의 에셋번들이 다운로드되어야 한다면, 동시에는 아주 적은 수의 에셋번들만 다운로드하도록 코드를 작성할 것을 추천드립니다.

3.4.4. AssetBundleDownloadHandler

유니티 5.3의 모바일 플랫폼에서 도입된 UnityWebRequest API는 유니티의 WWW API 보다 더 유연한 대안을 제공합니다. UnityWebRequest는 개발자들이 어떻게 유니티가 다운로드한 데이터를 다룰 지 정확히 설정할 수 있도록 해주고, 불필요한 메모리 사용을 없앨 수 있도록 해줍니다. UnityWebRequest를 통해 에셋번들을 다운로드할 수 있는 가장 간단한 방법은 UnityWebRequest.GetAssetBundle API를 이용하는 것 입니다.

이 가이드의 목적에 따라, 관심있는 부분은 DownloadHandlerAssetBundle 입니다. 이를 사용하면 WWW.LoadFromCacheOrDownload와 비슷하게 동작하게 됩니다. 작업 쓰레드를 이용해서 다운로드된 데이터를 고정 크기 버퍼에 스트리밍하고, 이렇게 버퍼에 담긴 데이터를 임시 저장 공간이나 에셋번들 캐쉬로 가져옵니다. 임시 저장 공간에 넣을 지, 에셋번들 캐쉬에 넣을 지는 Download Handler를 어떻게 설정했냐에 따라 결정됩니다. LZMA로 압축된 에셋번들은 다운로드 하는 중에 압축이 해제되고, 압축이 해제된 상태로 캐쉬되게 됩니다.

이런 모든 동작은 네이티브 코드 상에서 발생하게 되는데, 관리 힙을 늘리는 위험을 줄이기 위함입니다. 게다가 이 Download Handler는 모든 다운로드된 바이트에 대한 네이티브 코드 복사본을 유지하지 않고, 에셋번들을 다운로드할 때 발생하는 메모리 오버헤드를 줄입니다.

다운로드가 완료되었을 때,마치 다운로드된 에셋번들에 AssetBundle.LoadFromFile이 불린 것처럼, Download Handler의 assetBundle 속성은 다운로드된 에셋번들에 대한 접근을 제공합니다.

UnityWebRequest API는 WWW.LoadFromCacheOrDownload에 동일한 방법으로 캐슁을 지원합니다. 캐슁 정보가 Download Handler에 제공되고 요청한 에셋번들이 이미 유니티의 캐쉬에 존재한다면, 해당 에셋번들은 즉시 사용 가능한 상태가 되고 이 API는 AssetBundle.LoadFromFile과 동일하게 동작하게 됩니다.

메모 : 유니티의 에셋번들 캐쉬는 WWW.LoadFromCacheOrDownloadUnityWebRequest에서 공유됩니다. 둘 중 하나의 API로 다운로드된 에셋번들은 다른 하나의 API를 이용해 이용할 수 있게 됩니다.

메모 : WWW와는 다르게, UnityWebRequest 시스템은 작업 쓰레드에 대한 내부 풀(pool)과 내부 작업(job) 시스템이 있고, 이로 인해 개발자들은 초과 수량의 동시 다운로드를 할 수 없게 됩니다. 쓰레드 풀의 크기는 지금은 설정 불가능합니다.

3.4.5. 추천(Recommendations)

일반적으로, AssetBundle.LoadFromFile 이 가능하면 쓰여야 합니다. 이 API가 속도, 디스크 사용, 런타임 메모리 사용량에서 가장 효율적입니다.

에셋번들을 다운로드하거나 패치해야하는 프로젝트에서는, 유니티 5.3 이상에서는 UnityWebRequest를 사용할 것을 강력히 추천하고, 유니티 5.2 이하에서는 WWW.LoadFromCacheOrDownload를 사용할 것을 강력히 추천합니다. 다음 챕터에 나오는 배포 섹션에서 다루겠지만, 에셋번들 캐쉬를 프로젝트의 인스톨러에 있는 번들보다 높은 우선 순위를 가지게 하는 것이 가능합니다.

WWW.LoadFromCacheOrDownload를 사용할 때, 메모리 사용량의 갑작스러운 폭발로 인한 애플리케이션의 강제 종료를 예방하기 위해서, 프로젝트의 최대 메모리 예산이 2~3%보다 작게 에셋번들을 유지하는 것을 강력히 추천합니다. 대부분의 프로젝트에서, 에셋번들은 파일 크기로는 5MB를 넘어가지 않고, 동시에 1~2개의 에셋번들만 다운로드되어야 합니다.

WWW.LoadFromCacheOrDownloadUnityWebRequest를 사용할 때에, 에셋번들을 로드하고 나서는 다운로드 코드에서 적절하게 Dispose를 불러줘야 합니다. 대안으로는 C#의 using 문을 이용하는 것이 WWWUnityWebRequest를 안전하게 처리할 수 있는 가장 편리한 방법입니다.

실제 엔지니어링 팀에서 특별한 캐싱이나 다운로드가 필요한 프로젝트를 할 경우, 커스텀3 다운로드가 필요할 수도 있습니다. 커스텀 다운로더를 작성하는 것은 그리 쉬운 일이 아니고, 어떠한 커스텀 다운로더이던지 간에 AssetBundle.LoadFromFile과의 호환성이 지켜져야 합니다. 다음 챕터의 배포 섹션에서 더 자세한 내용을 다루겠습니다.

3.5. 에셋번들에서 에셋을 로드하기

에셋번들에서 UnityEngine.Object를 로드할 때에 AssetBundle 오브젝트에 붙어있는 3개의 API를 사용하면 됩니다 : LoadAsset, LoadAllAssets, LoadAssetWithSubAssets. 이 API들은 모두 비동기 방식에 대한 변형을 가지고 있고, 이들은 -Async라는 접미사가 붙습니다 : LoadAssetAsync, LoadAllAssetsAsync, LoadAssetWithSubAssetsAsync.

동기 API들은 비동기 API보다 최소한 1프레임은 더 빠릅니다. 이는 특히 유니티 5.1이하에서 맞습니다. 유니티 5.2 이전에 모든 비동기 API는 매 프레임마다 최대 하나의 오브젝트를 로드했습니다. 이는 LoadAllAssetsAsync와 LoadAssetsWithSubAssetAsync가 각각의 연관된 동기 API보다 심각하게 느릴 수 있음을 의미합니다. 유니티 5.2에 와서 이러한 동작이 개선되었는데요. 비동기 로드에서 매 프레임에 여러 개의 오브젝트를 로드하게 되는데, 최대량은 시간 조각(time-slice) 한계에 따라 결정됩니다. 이렇게 동작하는 기술적인 이유와 시간 조각(time-slicing)에 대한 더 자세한 내용을 알고 싶다면 3.5.1. 저 수준의 로딩 세부 사항 섹션을 읽어보세요.

LoadAllAssets 는 여러개의 독립된 UnityEngine.Object를 로드하는데에 사용됩니다. 이는 에셋번들의 대부분의 오브젝트가 로드되어야 하는 경우에만 사용하는 것이 좋습니다. 다른 2개의 API와 비교해서 LoadAllAssets는 LoadAssets를 여러번 호출하는 것보다 약간 더 빠릅니다. 따라서, 로드해야할 에셋의 수가 많고, 한 번에 로드되어야하는 수가 에셋번들 전체 컨텐츠의 2/3 이하인 경우에는, LoadAllAssets를 이용해서 그 에셋번들을 여러 개의 작은 번들로 나누는 것을 고려해보십시오.

LoadAssetWithSubAssets는 여러 개의 자식 오브젝트를 포함하는 복합 에셋을 로딩하는데에 사용되어야 하고, 이러한 에셋의 예로는 애니메이션을 여러 개 포함하고 있는 FBX 모델이라던지, 여러 개의 스프라이트를 포함하는 스프라이트 아틀라스같은 것이 있을 수 있습니다. 로드해야 할 오브젝트들이 모두 같은 에셋에 있는데, 다른 관계없는 오브젝트들이 많은 그러한 에셋번들에 저장되어 있는 경우에 이 API를 사용하십시오.

다른 경우에는 LoadAsset이나 LoadAssetAsync를 사용하십시오.

3.5.1. 저 수준의 로딩 세부 사항

UnityEngine.Object의 로딩은 메인 쓰레드 밖에서 일어납니다 : 저장 공간에서 오브젝트의 데이터를 읽는 것은 작업 쓰레드에서 일어납니다. 유니티의 쓰레드에 민감한 부분(스크립팅, 그래픽스)을 건드리지 않는 모든 것들은 작업 쓰레드에서 이루어 집니다. 예를 들어, VBO가 메쉬에서 생성되거나, 텍스쳐가 압축 해제되는 등이 있습니다.

유니티 5.3 이전에서는, 오브젝트 로딩은 순차적으로 이루어졌고, 오브젝트 로딩의 어떤 부분은 오직 메인 쓰레드에서만 이루어졌습니다. 이는 "통합(Integration)"이라고 부릅니다. 작업 쓰레드가 오브젝트의 데이터 로딩을 끝낸 후에, 새롭게 로드된 오브젝트를 메인 쓰레드로 통합시키기 위해 멈추는데, 메인 쓰레드의 통합작업이 끝나기 전까지 작업 쓰레드의 멈춤이 유지됩니다.

유니티 5.3 버전부터는, 오브젝트 로딩이 병렬적으로 진행되도록 변경되었습니다. 여러 개의 오브젝트들이 작업 쓰레드에서 역직렬화되고, 처리되어 통합되어 집니다. 오브젝트의 로딩이 끝나게 되면, Awake 콜백이 호출되고, 해당 오브젝트는 다음 프레임에 사용 가능하게 됩니다.

동기적인 AssetBundle.Load 메서드는 오브젝트의 로딩이 끝나기 전까지 메인 쓰레드를 정지시킵니다. 5.3 이전의 버전에서는 비동기적인 AssetBundle.LoadAsync 메서드는 메인 쓰레드에서 오브젝트를 통합하기 전까지는 메인 쓰레드를 정지시키지 않습니다. 물론 시간을 쪼개서 오브젝트를 로딩하기 때문에, 오브젝트 통합 과정에서 한 프레임에 일정 시간 이상의 시간을 잡아먹지는 않습니다. 한 프레임에 잡을 수 있는 시간은 Application.backgroundLoadingPriority 속성을 설정함으로써 변경할 수 있습니다.

  • ThreadPriority.High : 프레임 당 최대 50 ms
  • ThreadPriority.Normal : 프레임 당 최대 10 ms
  • ThreadPriority.BelowNormal : 프레임 당 최대 4 ms
  • ThreadPriority.Low : 프레임 당 최대 2 ms

유니티 5.1 이하에서, 비동기 API는 한 프레임에 하나의 오브젝트만 통합시킵니다. 이러한 동작은 버그로 인식되어 유니티 5.2에 수정되었습니다. 유니티 5.2부터는 오브젝트 로딩에 대한 프레임 시간 한계에 도달하기 전까지의 오브젝트들을 한꺼번에 로드합니다. AssetBundle.LoadAsync는 동기적 API보다 항상 더 오래 걸리는데, 이는 LoadAsync 호출을 하고나서부터 그 오브젝트가 쓸 수 있는 상태가 되기 전까지 최소 1프레임의 딜레이가 존재하기 때문입니다.

실제 오브젝트와 에셋을 가지고 테스트를 진행해보면 조금 다른 점을 볼 수 있습니다. 5.2 이전에는 저 사양 기기에서 꽤나 큰 텍스쳐를 로딩하는데에 동기적 메서드가 7 ms, 비동기적 메서드가 70 ms 걸렸습니다. 5.2 이후에는 그 차이가 거의 0에 가까워 졌습니다.

3.5.2. 에셋 번들 의존 관계

유니티 5의 에셋번들 시스템에서, 에셋번들 간의 의존성은 런타임 환경에 따라 자동으로 2개의 다른 API에 의해 추적되어 집니다. 유니티 에디터에서는, 에셋번들의 의존성은 AssetDatabase API를 통해서 확인할 수 있습니다. 에셋번들의 할당과 의존성은 AssetImporter API를 통해서 접근하고 변경될 수 있습니다. 런타임에는, 에셋번들 빌드 시에 만들어진 ScriptableObject 기반의 AssetBundleManifest API를 통해서 의존성 정보를 읽을 수 있습니다.

한 개 이상의 부모 에셋번들의 UnityEngine.Object가 한 개 이상의 다른 에셋번들의 UnityEngine.Object를 참조하고 있는 경우에 에셋번들이 다른 에셋번들에 "의존하고 있다"라고 말합니다. 오브젝트 내부 참조에 대한 더 세부적인 내용을 알고 있다면, 에셋과 오브젝트, 그리고 직렬화 글의 오브젝트 내부의 참조들 섹션을 참고해주세요.

저 글의 직렬화와 인스턴스 섹션에서 말하고 있듯이, 에셋번들은 에셋번들에 포함되어 있는 각 오브젝트의 File GUI와 Local ID에 의한 각 소스 데이터를 위한 소스 역할을 합니다.

오브젝트는 Instance ID가 처음으로 역참조되었을 때 로드되고, 에셋번들이 로드될 때 오브젝트에는 유효한 Instance ID가 할당되기 때문에, 에셋번들이 로드되는 순서는 중요하지 않습니다. 대신에, 오브젝트를 로드하기 전에 여기에 참조를 가지고 있는 모든 에셋번들을 로드하는 것이 중요합니다. 유니티는 부모 에셋번들이 로드될 때 자동으로 자식 에셋번들을 로드하지 않습니다.

예:

매터리얼 A가 텍스쳐 B를 참조한다고 가정해 보겠습니다. 매터리얼 A는 AssetBundle 1에 들어가 있고, 텍스쳐 B는 AssetBundle 2에 들어가 있습니다.

Asset bundle refering example


이러한 경우에 AssetBundle 2는 AssetBundle 1의 매터리얼 A가 로딩되기 이전에 로드되어야만 합니다.

이는 AssetBundle 1이 로드되기 전에 AssetBundle 2가 먼저 로드되어야만 함을 의미하지는 않습니다. 그리고 AssetBundle 2에서 Texture B가 명시적으로 먼저 로드되어야 함을 의미하지도 않습니다. AssetBundle 1의 매터리얼 A가 로드되기 전에 AssetBundle 2가 로드되어야 함을 의미합니다.

유니티는 AssetBundle 1이 로드될 때 자동으로 AssetBundle 2를 로드하지 않습니다. 이는 스크립트 상에서 수동으로 진행해줘야 합니다. AssetBundle 1과 2를 로드하는데에 사용되는 에셋번들 API는 상관없습니다. WWW.LoadFromCacheOrDownload를 통해서 로드되는 에셋번들은 AssetBundle.LoadFromFile이나 AssetBundle.LoadFromMemoryAsync를 통해서 로드된 에셋번들과 얼마든지 같이 사용될 수 있습니다.

3.5.3. AssetBundle manifests

BuildPipeline.BuildAssetBundles API를 통해서 에셋번들 빌드 파이프라인을 수행할 때, 유니티는 각 에셋번들의 의존성 정보에 포함된 오브젝트를 직렬화합니다. 이 데이터는 독립된 에셋번들에 저장되고, 여기에는 AssetBundleManifest 타입의 오브젝트가 하나 포함되어 집니다.

이 에셋은 빌드되는 에셋번들의 부모 디렉토리와 같은 이름의 에셋번들에 저장됩니다. 만약 프로젝트에서 (projectroot)/build/Client 폴더로 에셋번들을 빌드하게 되면, manifest를 포함하고 있는 에셋번들은 (projectroot)/build/Client/Client.manifest 에 저장되게 됩니다.

manifest를 포함하는 에셋번들 또한 다른 에셋번들처럼 로드될 수 있고, 캐쉬될 수 있고, 언로드될 수 있습니다.

AssetBundleManifest 오브젝트는 GetAllAssetBundles API를 제공하는데, 이 API를 통해 이 manifest에 동시에 빌드되는 모든 에셋번들을 리스트를 가져올 수 있습니다. 또한 특정 에셋번들의 참조를 가져올 수 있는 두 개의 메서드를 제공합니다.

AssetBundleManifest.GetAllDependencies는 에셋번들의 모든 참조를 반환하는데, 여기에는 에셋번들의 자식 뿐만 아니라, 그 하위 자식들까지도 모두 포함합니다.

AssetBundleManifest.GetDirectDependencies는 에셋번들의 자식까지만 반환합니다.

이 2개의 API는 모두 문자열 배열을 할당함을 알아 두십시오. 이 API는 너무 자주 쓰지 마시고, 애플리케이션의 생명주기에 있어서 성능에 민감한 부분에서는 사용하지 마십시오.

3.5.4. 추천

유저가 애플리케이션의 성능에 치명적인 부분에 들어서기 전에 가능한 필요한 오브젝트를 많이 로드하는 것이 좋습니다. 이런 데이터에는 메인 게임 레벨이라든지 월드 같은 것이 있습니다. 이는 특히 모바일 플랫폼에서 치명적인데, 로컬 저장 공간에 접근하는 것이 느리고, 플레이 시에 오브젝트를 메모리에 로드/언로드하는 것이 가비지 콜렉터가 동작하게 끔 만들 수 있습니다.

애플리케이션이 진행 중에 오브젝트를 로드/언로드 해야 하는 프로젝트의 경우에는, 에셋번들의 오브젝트를 언로드 하는 것에 대한 정보를 얻기 위해, 에셋번들 사용 패턴 글의 로드된 에셋의 관리 섹션을 보십시오.


  1. manifest는 일반적으로 소프트웨어에서 많이 사용되는 용어이므로 굳이 번역하지 않겠습니다. 단어 그대로 번역을 하자면 “적하 목록” 즉, 포함된 컨텐츠에 대한 리스트 정도로 생각하면 될 것 같습니다.

  2. raw data를 평소에 말할 때에는 그냥 “로우 데이터"라고 말하는데, 아무래도 번역을 하다보니 미가공 데이터라고 번역해도 괜찮을 것 같습니다.

  3. custom을 맞춤이라 해석할 수도 있지만, 보통 그냥 커스텀이라고 많이 읽고 쓰이기 때문에 따로 맞춤이라 번역하지 않겠습니다.

이 글은 유니티 튜토리얼을 번역한 글 입니다. 원문은 여기에서 확인하실 수 있습니다.

이 글은 총 5개의 챕터로 구성되며 아래와 같습니다.

  1. [번역] 에셋번들과 리소스에 대한 가이드
  2. [번역] 에셋과 오브젝트, 그리고 직렬화
  3. [번역] RESOURCES 폴더
  4. [번역] 에셋번들 기초
  5. [번역] 에셋번들 사용 패턴




이 글은 [번역] 에셋번들과 리소스에 대한 가이드 시리즈의 세번째 챕터입니다.

이 챕터에서는 Resources 시스템에 대해 이야기합니다. 이는 개발자가 Resources 라는 이름의 하나 이상의 폴더에 에셋을 저장하고, 런타임에 Resources API를 이용해서 오브젝트를 로드하거나 언로드할 수 있는 시스템을 말합니다.

2.1. Resources 시스템의 모범 사례(Best Practices)

이 시스템을 사용하지 마십시오.

이와 같은 강한 추천은 몇 가지 이유 때문에 만들어졌습니다.

  • Resources 폴더를 사용하는 것은 메모리에 대한 세세한 관리를 더 어렵게 만듭니다.
  • Resources 폴더를 부적절하게 사용하게 되면 애플리케이션의 시작 시간과 빌드의 크기를 증가시킵니다.
    • Resources 폴더의 수가 늘어날 수록, 이러한 폴더에 들어있는 에셋을 관리하는 것이 매우 어려워집니다.
  • Resources 시스템은 특정 플랫폼에 커스터마이징 된 컨텐츠를 프로젝트에서 사용할 수 없도록 하고, 점진적인 컨텐츠 업그레이드에 대한 가능성을 없애버립니다.
    • 에셋번들 변형(Variants)은 디바이스마다 컨텐츠를 조절할 수 있는 유니티의 주요 수단입니다.



2.2 Resources 시스템의 적절한 사용

좋은 개발 사례를 해치지 않고 Resources 시스템을 유용하게 사용할 수 있는 2가지 사용 예가 있습니다.

  1. Resources 는 빠른 프로토타이핑과 실험을 할 때에 최고의 시스템입니다. 왜냐하면 간단하고 사용하기 편하기 때문이죠. 하지만, 프로젝트가 최종 제품 단계에 들어가게 되면 Resources 폴더를 사용하지 않을 것을 강력히 추천합니다.
  2. Resources 폴더는 아래의 상황같은 흔한 경우에도 유용합니다.
    • Resources 폴더에 저장되어 있는 컨텐츠가 메모리에 별로 영향을 주지 않는 경우
    • 컨텐츠가 거의 프로젝트 전반적으로 필요한 경우
    • 컨텐츠의 패치가 거의 필요없는 경우
    • 컨텐츠가 플랫폼이나 디바이스에 따라 변경될 필요가 없는 경우


두번째 경우에 대한 예로는 전역으로 사용되는 싱글턴(singleton) MonoBehaviour나 페이스북 App ID같은 써드파티(third-party) 설정 데이터 에셋이 있습니다.

2.3. Resources의 직렬화

"Resources" 폴더에 있는 모든 에셋과 오브젝트는 프로젝트가 빌드될 때 단일 직렬화된 파일로 합쳐지게 됩니다. 이 파일은 또한 에셋번들과 비슷하게 메타데이터(metadata)와 익덱싱 정보도 포함하고 있습니다. 에셋번들 기초 챕터의 에셋번들 안에는 무엇이 있나? 섹션에서 말하고 있듯이, 이 인덱싱 정보는 직렬화된 색인(lookup) 트리(tree)를 포함하는데, 이는 오브젝트의 이름을 가지고 적합한 File GUID와 Local ID를 가져오는데에 쓰입니다. 또한 이 정보는 오브젝트를 직렬화된 파일의 내용에서 특정 바이트 오프셋에 위치시키기 위해 사용됩니다.

색인 자료 구조는 balanced search tree이기 때문에, 생성 시간은 O(N log(N)) 비율로 늘어나고, 여기에서 N은 트리에 익덱싱된 오브젝트의 개수입니다. 이런 생성 시간의 증가 비율은 인덱스의 로딩 시간이 Resources 폴더에 있는 오브젝트 갯수가 증가하는 것보다 더 크게 증가하게 만듭니다.

이런 동작은 넘어갈 수 없고(unskippable) 애플리케이션이 시작될 때 최초 스플래쉬 화면이 나올 때 발생하게 됩니다. 10,000 개의 에셋을 가지고 있는 Resources 시스템을 초기화하는데에 저사양 모바일 디바이스에서 수 초(multiple seconds)가 걸렸는데, Resources 폴더에 있는 오브젝트의 대부분이 애플리케이션의 첫 씬에 로드될 필요가 없었습니다.

이 글은 유니티 튜토리얼을 번역한 글 입니다. 원문은 여기에서 확인하실 수 있습니다.

이 글은 총 5개의 챕터로 구성되며 다음과 같습니다.

  1. [번역] 에셋번들과 리소스에 대한 가이드
  2. [번역] 에셋과 오브젝트, 그리고 직렬화
  3. [번역] RESOURCES 폴더
  4. [번역] 에셋번들 기초
  5. [번역] 에셋번들 사용 패턴




이 글은 [번역] 에셋번들과 리소스에 대한 가이드 시리즈의 두번째 챕터입니다.

이 챕터에서는 유니티의 직렬화 시스템에 대해 심도있게 다루고, 유니티 에디터에서 그리고 실행시(runtime)에 다른 오브젝트 간의 참조를 어떻게 유지하는지에 대해 이야기 합니다. 또한 여기에서는 오브젝트(Objects)와 에셋(Assets) 간의 기술적인 차이점에 대해서도 이야기 합니다. 여기에서 다루는 주제는 유니티에서 어떻게 효율적으로 에셋을 로드하고 언로드하는지에 대한 근본적인 이해를 다룹니다. 적절한 에셋 관리는 로딩 시간을 줄이고 메모리를 적게 사용하는 데에 있어 필수적인 요소입니다.

1.1. 에셋과 오브젝트의 내부(Inside Assets and Objects)

유니티에서 어떻게 데이터를 적절히 다루는지를 이해하기 위해, 유니티가 어떻게 데이터를 구별하고(identifies)1 직렬화하는지를 이해하는 것이 매우 중요합니다. 첫번째 핵심은 Asset과 UnityEngine.Object를 구별하는 것입니다.

에셋(Asset)은 유니티 프로젝트의 Assets 폴더에 저장되어 있는 디스크 상의 한 파일입니다. 예를들어 텍스쳐 파일들, 매터리얼 파일들, FBX 파일들이 모두 에셋입니다. 매터리얼과 같은 일부 에셋은 유니티의 네이티브 포맷 데이터를 포함하고 있습니다. FBX 파일 같은 다른 에셋들은 네이티브 포맷으로의 변환 작업이 필요합니다.

오브젝트(UnityEngine.Object) 또는 Object(대문자 O로 시작해야 함)는 리소스의 특정 인스턴스를 표현하는 직렬화된 데이터들의 집합입니다. 이는 유니티에서 사용하는 어떤 리소스 타입도 가능한데, 예를 들어, 메쉬(mesh), 스프라이트(sprite), AudioClip, AnimationClip 등이 있습니다. 모든 종류의 오브젝트는 UnityEngine.Object를 상속받은 하위 클래스입니다.

대부분의 오브젝트 타입은 유니티에 이미 만들어져 있는 타입(built-in)이지만, 2개의 특별한 타입 또한 존재합니다.

  1. ScriptableObject는 개발자들이 자신만의 데이터 타입을 정의할 수 있게 해주는 편리한 시스템입니다. 이러한 타입은 유니티에서 네이티브적으로 직렬화(serialized) 또는 역직렬화(deserialized)할 수 있고, 유니티 에디터의 인스펙터 창에서 조작할 수 있습니다.
  2. MonoBehaviourMonoScript로의 링크를 가지는 래퍼(wrapper)2를 제공합니다. MonoScript는 내부 데이터 타입인데, 유니티는 특정 스크립팅 클래스가 특정 어셈블리와 네임스페이스에 대한 참조를 유지하는데에 이 MonoScript를 사용합니다. MonoScript는 실질적인 실행 가능한 코드를 전혀 포함하지 않고 있습니다.

에셋과 오브젝트 간에는 일대다(one-to-many)의 관계가 있습니다 : 이 말은 어떤 하나의 에셋 파일이 한 개 이상의 오브젝트를 가진다는 말입니다.

1.2. 오브젝트 내부의 참조들(Inter-Object References)

모든 UnityEngine.Object는 다른 UnityEngine.Object에 대한 참조를 가질 수 있습니다. 여기에서 참조가 되는 오브젝트는 같은 에셋 파일 내부에 있을 수도 있고, 다른 에셋 파일에서 임포트될 수도 있습니다. 예를들어 하나의 매터리얼(Material) 객체는 일반적으로 한개 이상의 텍스쳐(Texture) 객체에 대한 참조를 가지고 있습니다. 또 이 텍스쳐 객체들은 일반적으로 한개 이상의 텍스쳐 에셋 파일(PNG나 JPG같이)로 부터 임포트됩니다.

직렬화되었을 때 이 참조들은 두 개의 데이터 조각으로 구성됩니다 : File GUID와 Local ID로 말입니다. File GUID는 에셋 파일의 타겟 리소스가 어디에 저장되어 있는지를 가리키기 위해 사용됩니다. 내부적으로 유일한 Local ID는 에셋 파일 내의 각 오브젝트를 가리키는데, 이는 하나의 에셋 파일에 여러개의 오브젝트가 있을 수 있기 때문입니다.

File GUID는 .meta 파일에 저장되어 있습니다. 이 .meta 파일은 유니티가 처음 어떤 에셋을 임포트할 때 생성이 되는데 그 에셋이 있는 같은 폴더에 저장됩니다.

위에서 말한 식별화(identification)와 참조 시스템(referencing system)은 텍스트 에디터에서 확인이 가능합니다 : 새 유니티 프로젝트를 생성하고 Editor Settings에서 Visible Meta Files가 나오도록 설정하고, 에셋을 텍스트로 직렬화하도록 설정합니다. 매터리얼을 생성하고 프로젝트에 텍스쳐를 하나 임포트합니다. 이 매터리얼을 씬(Scene)에 있는 큐브에 할당하고 씬을 저장합니다.

텍스트 에디터를 이용해서 이 매터리얼과 관련된 .meta 파일을 엽니다. 파일의 거의 제일 윗 부분에 "guid"라 적혀 있는 줄을 볼 수 있을 것입니다. 이 줄이 매터리얼 에셋의 File GUID를 정의하는 줄입니다. Local ID를 찾으려면 매터리얼 파일을 텍스트 에디터에서 여십시오. 매터리얼 객체의 정의는 다음과 같을 것입니다.

--- !u!21 &2100000
Material:
 serializedVersion: 3
 ... more data ...

위의 예에서 앰퍼샌드( &)로 시작하는 숫자가 이 매터리얼의 Local ID입니다. 만약 이 매터리얼 객체가 File GUID "abcdefg"인 에셋 안에 위치해 있다면, 이 매터리얼 객체는 File GUID "abcdefg"와 Local ID "2100000"을 조합해서 유일한 식별자를 가질 수 있습니다.

1.3. 왜 File GUID와 Local ID인가?

왜 유니티의 File GUID와 Local ID 시스템이 필요한 것일까요? 그에 대한 답은 강건함(robustness)과 플랫폼에 독립적인 워크 플로우를 제공하기 위해서 입니다.

File GUID는 한 파일의 특정 위치에 대한 추상화를 제공합니다. 특정 File GUID가 특정 파일과 연관이 있을 수 있는 한, 디스크 상에서의 그 파일의 위치는 무관해집니다. 그렇기 때문에 한 파일은 그 파일을 참조하고 있는 모든 오브젝트를 업데이트할 필요없이 마음대로 옮길 수 있습니다.

어떤 에셋 파일은 여러 개의 UnityEngine.Object 리소스를 가질 수 있기 때문에, Local ID는 각 오브젝트들을 명확하게 구분하는 데에 필요합니다.

어떤 에셋 파일과 관련된 File GUID를 잃어버리게 된다면, 그 에셋 파일에 있는 모든 오브젝트에 대한 참조를 잃어버리게 됩니다. 이러한 이유 때문에 .meta 파일이 같은 폴더에 같은 파일명으로 저장되어야만 하는 것입니다. 유니티는 삭제되거나 위치가 잘 못된 .meta 파일을 재생성한다는 것을 알아두면 좋겠습니다.

유니티 에디터는 특정 파일 경로가 File GUID를 가리키고 있는 맵(map)을 하나 가지고 있습니다. 맵의 각 항목은 에셋이 로드되거나 임포트될 때마다 기록되어집니다. 맵 항목은 에셋의 특정 경로와 그 에셋의 File GUID 간의 링크를 가지고 있습니다. 만약 유니티 에디터가 켜졌을 때 .meta 파일이 없어진 상태이고 그 에셋의 경로가 바뀌지 않았다면, 에디터는 해당 에셋이 같은 File GUID를 보유하고 있다고 확신하게 됩니다.

유니티 에디터가 닫힐 때 .meta 파일이 없어진 상태거나, 에셋의 경로는 바뀌었는데 .meta 파일은 해당 에셋과 같은 경로로 이동하지 않았다면, 해당 에셋에 있는 모든 객체에 대한 참조는 깨지게 됩니다.

1.4. 복합 에셋(Composite Assets)과 임포터(Importers)

에셋과 오브젝트의 내부에서 이야기했듯이, 네이티브가 아닌 타입의 에셋은 유니티에 임포트되어져야만 합니다. 이는 에셋 임포터를 통해서 이루어 집니다. 이러한 임포터는 보통은 자동으로 호출되어지지만, AssetImporter API와 AssetImporter의 하위 클래스를 통해서 스크립트로도 노출되어져 있습니다. 예를 들면, TextureImporter API는 PNG나 JPG같은 개별 텍스쳐 에셋을 임포트할 때 사용되는 셋팅에 대한 접근을 제공합니다.

임포트 프로세스의 결과는 한 개 이상의 UnityEngine.Object 입니다. 이들은 부모 에셋(parent Asset) 안에 있는 여러 개의 서브 에셋(sub-assets) 형태로 유니티 에디터에서 볼 수 있는데, 예를 들면 sprite atlas로 임포트되어진 하나의 텍스쳐 에셋에 보면 여러개의 sprite들이 있는 것을 알 수 있습니다. 이런 오브젝트들은 각각 하나의 File GUID를 공유하게 되는데, 이는 이들의 소스 데이터가 같은 에셋 파일에 저장되어 있기 때문입니다. 이들은 임포트된 텍스쳐 에셋 내에서 Local ID를 이용해 구분되어 집니다.

임포트 프로세스는 소스 에셋을 유니티 에디터에서 선택한 타겟 플랫폼에 맞는 포맷으로 변환합니다. 임포트 프로세스에는 텍스쳐 압축과 같은 매우 무거운 작업들을 포함시킬 수도 있습니다. 유니티 에디터가 열릴 때마다 임포트 프로세스를 수행하는 것은 엄청나게 비효율적일 수 있습니다.

대신에, 임포트된 에셋의 결과물은 Library 폴더에 캐쉬됩니다. 특히 임포트 프로세스의 결과물은 에셋의 File GUID 의 앞 두자리로 된 폴더에 저장됩니다. 이 폴더는 Library/metadata/ 폴더 안에 있습니다. 각 오브젝트는 하나의 바이너리 파일로 직렬화되는데, 이 파일의 이름은 에셋의 File GUID와 동일합니다.

이 내용은 네이티브가 아닌 에셋(non-native Assets)에 대해서만이 아닌 모든 에셋에 대해서 적용되는 내용입니다. 하지만, 네이티브 에셋의 경우는 긴 변환 프로세스나 재직렬화(re-serialization) 작업이 필요하지는 않습니다.

1.5. 직렬화(Serialization)와 인스턴스(Instances)3

File GUID와 Local ID는 강건하지만, GUID 비교는 느리고 런타임에 더 높은 퍼포먼스를 가진 시스템이 필요합니다. 유니티는 내부적으로 캐쉬를 유지하고 있는데, 이는 File GUID와 Local ID를 하나의 세션 동안에만 유일함이 보장되는 간단한 정수로 변환합니다. 이를 Instance ID라 부르고, 새로운 오브젝트가 캐쉬에 등록될 때 단조롭게 증가되는(monotonically-increasing) 값입니다.

이 캐쉬는 오브젝트의 소스 데이터 위치를 정의하는 Instance ID, File GUID, Local ID 와 메모리 상의 오브젝트 인스턴스 사이의 매핑을 가지고 있습니다. 이로 인해 UnityEngine.Object가 서로 간의 참조를 강건하게 유지할 수 있게 됩니다. Instance ID 참조를 제거하는 것은 해당 Instance ID를 가지고 있는 로드된 오브젝트를 바로 반환하는 결과를 낳을 수 있습니다. 만약 타겟 오브젝트가 아직 로드되지 않았다면, 해당 File GUID와 Local ID를 이용해 오브젝트의 소스 데이터를 확인할 수 있고, 유니티는 그 때 곧바로 해당 오브젝트를 로드할 수 있습니다.

시작 시점에, Instance ID 캐쉬는 해당 프로젝트에 들어 있는(예를 들면, 씬 내부에 참조되어 있는 오브젝트) 모든 오브젝트와 Resources 폴더에 있는 모든 오브젝트에 대한 데이터를 가지고 초기화되어 집니다. 추가적인 캐쉬는 런타임에 새로운 에셋이 임포트되거나 에셋번들(AssetBundle)을 통해서 오브젝트가 로드될 때 생성됩니다. Instance ID 항목은 필요없어 질 경우에만 캐쉬에서 제거됩니다. 이는 특정 File GUID와 Local ID에 대한 접근을 제공하는 에셋번들이 언로드될 때 발생합니다.

에셋번들을 언로드함으로써 Instance ID가 쓸모없게 되어 버리면, Instance ID와 File GUID, Local ID 간의 매핑은 메모리 절약을 위해 지워지게 됩니다. 만약 에셋번들이 다시 로드되면, 해당 에셋번들에서 로드된 각 오브젝트에 새로운 Instance ID가 할당되게 됩니다.

에셋번들의 언로딩에 관련된 더 심도있는 토론을 보고 싶다면, 에셋번들 사용 패턴 글에서 로드된 에셋의 관리 섹션을 보시면 됩니다.

특정 플랫폼에서 어떤 이벤트들이 오브젝트들을 강제로 메모리 상에서 제거하는 일도 있습니다. 예를 들어, iOS에서 앱이 중지됐을 때(suspended) 그래픽 에셋이 그래픽 메모리에서 언로드될 수 있습니다. 만약 이 오브젝트들이 에셋번들에서 로드한 객체이고 그 에셋번들이 이미 언로드된 상태라면, 유니티는 해당 오브젝트에 대한 소스 데이터를 다시 로드할 수 없을 것 입니다. 또한 언로드된 오브젝트를 참조하고 있는 다른 살아있는 오브젝트 또한 제대로 동작할 수 없는 상태가 됩니다. 앞선 예에서, 메쉬나 모델의 경우는 보이지 않게 되고, 텍스쳐와 매터리얼의 경우에는 보라색(magenta)으로 칠해지게 됩니다.

구현 노트 : 런타임에 위의 제어 흐름은 완전히 정확하게 발생하는 것은 아닙니다. 런타임에 File GUID와 Local ID를 비교하는 것은 무거운 로딩 작업을 할 때 좋지 않은 퍼포먼스를 낼 수도 있습니다. 유니티 프로젝트를 빌드할 때, File GUID와 Local ID는 결정적으로 더 간단한 포맷으로 매핑되어 집니다. 하지만, 위에서 말한 컨셉은 그대로 유지되고, 런타임에 File GUID와 Local ID에 대해 고려하는 것은 유용할 수 있습니다.

이러한 이유로 런타임에 에셋의 File GUID를 알아낼 수 없습니다.

1.6. MonoScripts

하나의 MonoBehavior는 하나의 MonoScript에 대한 참조를 가지고 있고, MonoScript는 단순히 특정 스크립트 클래스를 위치시키기 위한 정보만 가지고 있다는 것을 이해해야 합니다. 어떤 타입의 오브젝트도 스크립트 클래스에 대한 실행 가능한 코드를 가지고 있지는 않습니다.

MonoScript는 3개의 문자열을 가지고 있습니다 : 어셈블리 이름, 클래스 이름, 네임 스페이스

프로젝트를 빌드할 때, 유니티는 에셋 폴더에 있는 스크립트 파일들을 모으고, 이들을 Mono 어셈블리에 컴파일합니다. 특히, 유니티는 Assets 폴더 안에 있는 각 언어별로 어셈블리를 생성하게 되고, 각각의 언어별 어셈블리들은 Assets/Plugins 폴더 아래에 위치하게 됩니다. Plugins 하위 폴더의 밖에 있는 C# 스크립트는 Assembly-CSharp.dll에 위치하게 되고, Plugins 하위 폴더 안에 있는 스크립트는 Assembly-CSharp-firstpass.dll에 위치하게 됩니다.

이런 어셈블리 파일들은 (미리 빌드된 어셈블리 DLL도 포함해서) 유니티 애플리케이션의 최종 빌드에 포함되게 되고, 이들을 MonoScript가 참조하게 됩니다. 다른 리소스와는 다르게, 유니티 애플리케이션에 포함된 모든 어셈블리들은 애플리케이션이 최초 실행될 때 로드됩니다.

이런 MonoScript 오브젝트가 있기 때문에, 에셋번들(또는 Scene이나 Prefab)이 실행 가능한 코드를 어떤 MonoBehavior Component에도 포함시키지 않는 이유가 됩니다. 그렇게 때문에 MonoBehavior들이 서로 다른 에셋번들에 위치해 있더라도, 각각의 MonoBehavior들이 특정 공유 클래스를 참조할 수 있는 것입니다.

1.7. 리소스 생명주기(Resource Lifecycle)

UnityEngine.Object는 어떤 정해진 시간에 메모리에 로드되고 언로드됩니다. 로딩 시간을 줄이고 애플리케이션의 메모리 공간을 관리하기 위해서 UnityEngine.Object의 리소스 생성주기를 이해하는 것은 중요합니다.

UnityEngine.Object를 로드하는 데에는 두 가지 방법이 있습니다 : 자동이거나 명시적이거나. Instance ID가 매핑되었는데, 해당 오브젝트의 참조가 끊어졌고, 그 오브젝트가 메모리에 로드되지 않은 상태이며, 그 오브젝트의 소스 데이터를 로드할 수 있는 상태일 때에 해당 오브젝트는 자동으로 로드되게 됩니다. 반면에 명시적으로 로드할 때에는 직접 오브젝트를 생성하거나 리소스 로딩 API(예를 들면, AssetBundle.LoadAsset)를 사용하는 방법이 있습니다.

오브젝트가 로드될 때, 유니티는 각 참조에 대한 File GUID와 Local ID를 Instance ID로 변환함으로써 해당 오브젝트의 참조를 연결하게 됩니다.

아래의 2가지 항목이 만족되면서 Instance ID가 처음으로 역참조(dereferenced)될 때, 그 객체가 로드되어 집니다.

  1. Instance ID가 아직 로드되지 않은 오브젝트를 참조할 때
  2. Instance ID가 캐쉬에 등록된 유효한 File GUID와 Local ID를 가지고 있을 때

일반적으로 참조가 로드되고 연결되는데에는 굉장히 짧은 시간이 걸립니다.

만약 File GUID와 Local ID가 Instance ID를 가지고 있지 않거나, 언로드된 오브젝트의 Instance ID가 유효하지 않은 File GUID와 Local ID를 참조하고 있다면, 참조는 유지가 되지만 실제 오브젝트는 로드되지 않습니다. 이는 유니티 에디터 상에서 "(Missing)" 참조로 보여질 것 입니다. 동작 중인 애플리케이션에서나 Scene 뷰에서 "(Missing)" 오브젝트는 타입에 따라 다른 방식으로 보여질 것 입니다 : 메쉬는 보이지 않고, 텍스쳐는 magenta 색상으로 보이는 등 말이죠.

오브젝트는 3가지의 특정 시나리오대로 언로드 됩니다.

  1. 사용하지 않는 에셋에 대한 청소(cleanup)가 발생할 때 오브젝트는 자동으로 언로드됩니다. 이 프로세스는 scene이 변경되거나(Application.LoadLevel 처럼 additive 방식이 아닌 씬 변경 API를 사용하는 경우), 스크립트가 Resources.UnloadUnusedAssets API를 호출함으로써 발생합니다. 이 프로세스는 참조되지 않은 오브젝트만 언로드합니다 : 아무 Mono 변수도 해당 오브젝트를 참조하지 않고, 그 오브젝트를 참조하고 있는 다른 살아있는 오브젝트가 하나도 없는 경우에 그 오브젝트는 언로드됩니다.
  2. Resources 폴더에서 생성한 오브젝트는 Resources.UnloadAsset API를 호출함으로써 명시적으로 언로드될 수 있습니다. 이러한 오브젝트의 Instance ID는 유효한 상태로 남아있게 되고, 유효한 File GUID와 Local ID 항목을 포함하게 됩니다. 만약 Resources.UnloadAsset을 이용해서 언로드한 오브젝트에 대한 참조를 어떤 Mono 변수나 다른 오브젝트가 가지고 있다면, 살아있는 참조에 대한 역참조가 발생하자마자 언로드한 오브젝트는 다시 로드되게 됩니다.
  3. Asset Bundle에서 생성한 오브젝트는 AssetBundle.Unload API를 호출하면 즉시 자동 언로드되게 됩니다. 이는 해당 오브젝트의 Instance ID의 File GUID와 Local ID에 대한 참조를 무효화시키고(invalidates), 언로드한 오브젝트를 참조하고 있는 살아있는 어떤 참조들은 "(Missing)" 참조 상태가 될 것 입니다. C# 스크립트에서 언로드한 오브젝트의 메서드나 프로퍼티에 접근하려고 시도하면 NullReferenceException을 발생시키게 됩니다.

만약 AssetBundle.Unload 가 호출되면, 언로드 된 에셋번들에서 생성된 살아있는 오브젝트들은 파괴되지 않고, 유니티는 그런 오브젝트의 Instance ID에 대한 File GUID와 Local ID 참조를 무효화시킵니다. 만약 이런 오브젝트가 나중에 메모리에서 언로드되면 유니티에서는 이런 오브젝트들을 리로드할 수 없게 되고, 언로드된 오브젝트에 대한 살아있는 참조는 여전히 남아있게 됩니다.

1.8. 큰 Hierarchies를 로드하는 것(Loading Large Hierarchies)4

유니티 GameObject의 hierarchies를 직렬화할 때(예를 들어 Prefab), hierarchy 전체가 직렬화된다는 사실을 알아야 합니다. 그 말인 즉슨, 그 hierarchy에 있는 모든 GameObject와 Component들은 개별적으로 직렬화된 데이터 형태로 표현이 된다는 말입니다. 이는 GameObject의 hierarchy를 로드하고 생성하는데에 걸리는 시간에 흥미로운 영향을 미칩니다.

어떤 코드 덩어리가 같은 갯수의 최종 GameObject를 생성한다고 가정하면, 매우 큰 hierarchy를 가지는 하나의 prefab을 생성하는 것이 작은 오브젝트를 각각 생성해서 런타임에 조립하는 것보다 더 많은 CPU 시간을 소모합니다.

deep profiling5 자료를 분석해보면 GameObject를 생성하고 깨우는데에(awaken) 위의 두 시나리오가 거의 같습니다. 하나의 큰 hierarchy를 가지는 prefab의 경우가 아주 약간의 CPU 시간에 있어서 이득을 가집니다(trampolining6과 SendTransformChanged 콜백 때문에). 하지만 이런 약간의 시간 절약이 데이터를 읽고 역직렬화하는 데에 사용되는 시간에 있어서 매우 중요합니다.

앞에서 말했듯이, 단일 prefab을 직렬화할 때 각각의 모든 GameObject와 컴포넌트의 데이터가 개별적으로 직렬화됩니다 - 데이터가 중복인 경우에도 개별적으로 되기는 마찬가지 입니다. 30개의 동일한 요소가 있는 UI 화면은 그 동일한 요소를 30번 직렬화할 것 입니다. 이는 아주 큰 크기의 바이너리 데이터를 생성하게 됩니다. 로드 시점에 이 중복된 30개의 요소에 대한 모든 GameObject와 Component를 위한 데이터는 새로 생성된 데이터에 전송되기 전에 디스크에서 읽어야 합니다. 이렇게 큰 prefab을 생성하는데에 가장 많은 영향을 미치는 부분이 파일을 읽는 시간입니다.

유니티가 중첩된(nested) prefab을 지원하지 전까지는, 매우 큰 GameObject의 hierarchy를 생성하는 프로젝트에서 유니티의 직렬화와 prefab 시스템에 전적으로 의존하기보다는, 재사용되는 요소를 이용해 prefab을 분리 시키고 런타임에 이를 생성함으로써 큰 prefab에 대한 로딩 시간을 극적으로 줄일 수 있을지도 모릅니다.


  1. identify를 어떻게 번역하는게 좋을까 고민하다가 여기에서는 다른 것과 다름을 구별한다의 의미가 강하므로 구별하다로 표현했습니다.

  2. wrapper를 한글로 어떻게 번역하는게 좋을지 모르겠네요. 평소에 그냥 래퍼라고 발음 그대로 읽어왔기 때문에 따로 번역할 단어를 못 찾겠습니다.

  3. instance를 객체라고 번역할까 하다가 Object와 혼돈의 여지가 너무 큰 것 같아서 그냥 인스턴스라고 표기했습니다.

  4. hierarchy를 계층이라고 표현할 수도 있지만 유니티에서 흔히 사용되는 용어이므로 계층이라고 번역하는 것보다는 그냥 용어 자체를 고유명사처럼 사용하는게 더 나을 것 같습니다.

  5. deep profiling은 유니티에서 제공하는 기능이므로 굳이 한글로 번역하지 않는 것이 더 나을 것 같습니다.

  6. trampolining이 컴퓨터 분야에서 어떤 의미로 쓰이는지 모르겠네요.

이 글은 유니티 튜토리얼을 번역한 글 입니다. 원문은 여기에서 확인하실 수 있습니다.

이 글은 총 5개의 챕터로 구성되며 아래와 같습니다.

  1. [번역] 에셋번들과 리소스에 대한 가이드
  2. [번역] 에셋과 오브젝트, 그리고 직렬화
  3. [번역] RESOURCES 폴더
  4. [번역] 에셋번들 기초
  5. [번역] 에셋번들 사용 패턴




이 시리즈는 유니티 엔진에서 에셋1과 리소스를 관리하는 것에 대한 깊이있는 이야기를 하고자 합니다. 유니티의 에셋과 직렬화 시스템에 대한 심도있는 소스 단계(source-level) 지식을 전문 개발자에게 제공하고자 합니다. 이 글에서는 유니티 에셋번들 시스템의 기술적인 뒷받침들과 현재 그것들을 적용시키기 위한 모범 사례들을 검토합니다.

이는 총 4개의 챕터로 구성되어 있습니다.

  • [번역] 에셋과 오브젝트, 그리고 직렬화 -이 글에서는 유니티가 어떻게 에셋을 직렬화하고 에셋 간의 참조를 다루고 있는지에 대한 저수준의 상세한 내용을 다룹니다. 전체 가이드에서 사용되는 기술 용어를 정의하고 있는 챕터이므로, 독자들은 이 챕터부터 읽어볼 것을 강력히 추천합니다.
  • [번역] RESOURCES 폴더 - 이 글에서는 내장되어 있는 Resources API에 대해 이야기합니다.
  • [번역] 에셋번들 기초 - 이 글은 챕터 1에 기반하여, 에셋 번들이 어떻게 동작하는지와 에셋 번들을 로딩하는 부분, 에셋 번들에서 에셋을 로딩하는 부분에 대한 이야기를 다룹니다.
  • [번역] 에셋번들 사용 패턴 - 이 글은 에셋 번들의 실질적인 사용에 관련한 많은 주제를 다루고 있습니다. 이 글은 몇개의 섹션(section)으로 구성되어 있는데, 에셋을 에셋번들에 할당하는 것, 로드된 에셋을 관리하는 것, 개발자가 에셋 번들을 사용할 때에 직면하게 되는 많은 함정에 대해 다룹니다.


주의 : 이 가이드에서 사용되는 오브젝트(Objects)와 에셋(Assets)은 유니티의 API 네이밍 관습(convention)과는 다릅니다.

이 가이드에서는 데이터(data)를 오브젝트(Objects)라 말하고 이는 많은 유니티 API에서 에셋이라 불립니다. 예를들면 AssetBundle.LoadAsset 이나 Resources.UnloadUnusedAssets 처럼 말이죠.

반면 이 가이드에서 파일(files)은 에셋(Assets)이라 말하고 이는 유니티 API에서는 거의 사용되지 않고 있습니다. 이것이 유니티 API에서 사용되는 경우에는 AssetDatabaseBuildPipeline 같은 빌드 관련된 코드에서만 사용되고 있습니다. 이 경우에 유니티 API에서는 이를 파일(files)이라 지칭하고 있습니다.


  1. Asset의 “A"가 보통 한글로 표기될 때 "애"로 표기되지만 구글링을 해본 결과 오히려 "에셋"이라는 표기가 많이 쓰이고 있어서 저자도 "에셋"이라 표기하기로 결정했습니다.

+ Recent posts