유니티 엔진을 이용한 게임 개발 과정에서 패치 시스템 등을 위한 도구로서 에셋 번들이 자주 사용되는데 번들의 효율적인 관리와 사용을 위해서 모든 에셋들을 한 번들에 묶지 않고, 필요한 분류에 따라서 여러 번들에 나누어서 묶어서 사용하게 된다. 예를 들자면 캐릭터에 사용되는 리소스와 에셋들만 모아서 "character"라는 이름의 번들로 묶거나, UI에 사용되는 이미지들만 모아서 "ui_texture"라는 이름의 번들로 묶는 것이다.
이렇게 분류별로 나누어둔 번들은 다른 포스트에서 언급(http://wergia.tistory.com/29)했듯이 다음과 같이 BuildAssetBundles() 라는 함수를 통해서 빌드할 수 있다. 다음은 그 예시이다.
위의 예시를 통해서 묶어둔 에셋들을 번들로 빌드할 수 있는데, 이 코드를 사용할 경우, 당신이 지정한 모든 번들들을 한꺼번에 빌드한다. 이것은 에셋 번들을 테스트로 빌드하거나, 적은 수의 혹은 작은 용량의 번들을 빌드할 때는 인식하지 못했던 문제를 발생시킨다. 이 문제는 개발자가 게임을 개발하는데 투자하는 가장 중요한 자원을 소모시킨다. 그 자원은 바로 "시간"이다.
작은 용량의 에셋을 번들로 빌드할 때는 고작 몇 초의 시간이 걸릴 뿐이지만, 수 GB의 에셋들을 번들로 빌드할 때는 5-10분, 혹은 그보다 많은 시간을 소모하게 된다. 만약에 가장 작은 크기의 번들에 들어가는 에셋을 수정했는데 새로이 번들을 빌드하기 위해 모든 에셋번들을 빌드해야 한다면 얼마나 많은 시간을 무의미하게 소모하게 될 것인가? 필요한 번들만을 빌드했다면 고작 수십 초에서 2-3분의 시간만을 소모했을 작업을 수 분, 수십 분을 소모해야 한다면 이 얼마나 불합리한 일인가?
필요한 번들만을 빌드하는 방법으로 유니티에서는 위의 코드와 같은 BuildAssetBundles() 함수의 오버로드를 제공한다. 원래 에셋 번들 빌드에 사용하던 함수에서 AssetBundleBuild라는 구조체 형식의 매개변수가 추가되었는데, 빌드하고자 하는 번들의 정보를 담는 구조체이다. 원하는 에셋 번들을 빌드하기 위해 제공해야할 정보들은 다음과 같다.
public struct AssetBundleBuild { public string assetBundleName; // 빌드할 에셋 번들의 이름 public string assetBundleVariant; // 빌드할 에셋 번들의 Variant public string[] assetNames; // 에셋 번들에 포함될 에셋들의 경로와 이름 }
빌드하고자 하는 에셋 번들에 대한 AssetBundleBuild 구조체 혹은 구조체 배열을 만든 뒤, 빌드할 에셋 번들의 이름, Variant(variant가 없다면 넣지 않아도 된다.), 에셋번드레 포함될 에셋들의 경로와 이름을 넣어주고 BuildAssetBundles() 함수의 2번째 매개변수로 전달하고 함수를 실행하게 되면 한 번에 모든 에셋 번들들을 빌드할 필요없이 원하는 에셋 번들만을 빌드할 수 있게 된다.
빌드하고자 하는 에셋 번들에 포함될 에셋들 찾기
원하는 에셋 번들만을 빌드하고자 할 때는 위에서 봤듯이 AssetBundleBuild 구조체를 만들어 필요한 정보들을 채워넣어 주어야 하는데, 그 중에서 에셋 번들에 포함될 에셋들의 경로와 이름인 assetNames의 경우에는 수동으로 입력하는 것은 매우 불편할 뿐더러 프로젝트가 커지면 커질 수록 관리하기도 힘들고 어떤 에셋이 어느 번들에 포함되기로 되어있는지 기억하기도 힘들어질 것이다.
string assetBundleName // 찾고자 하는 에셋들이 포함된 에셋 번들 이름
);
위의 함수를 사용하면 유니티 에디터의 Inspector 창에서 해당 번들 이름으로 지정해둔 모든 에셋들의 경로와 이름을 string의 배열 형태로 반환한다. 이것을 AssetBundleBuild의 assetNames에 넣어주면 간편하게 번들에 포함될 에셋들의 정보를 AssetBundleBuild 구조체에 넣을 수 있다.
빌드하고자 하는 에셋 번들만을 빌드하는 간단한 예제 함수의 전체는 다음과 같다.
public void BuildNeedAssetBundle(string bundleName) { if (!Directory.Exists(Application.dataPath + "/AssetBundles")) { Directory.CreateDirectory(Application.dataPath + "/AssetBundles"); }
AssetBundleBuild[] buildBundles = new AssetBundleBuild[1];
요 며칠간 패치 시스템을 만들면서 아주 많이 고민해야 했던 문제가 있었는데 그것이 바로 AssetBundleManifest 오브젝트를 불러오는 방법이었다. 여러가지 방법을 이용해서 에셋 번들의 매니페스트 파일을 불러오는 것을 시도했으나 번번히 AssetBundleManifest 객체는 Null 값을 뱉으며 실패하는 경험을 했다. 이를 해결하기 위해서 구글링해서 발견한 코드도 적용해보고, AssetBundleManager의 코드도 참고해 봤지만 문제는 해결될 기미가 보이지 않았다.
제일 처음 기본적으로 참고한 유니티 5.6 문서에서는 AssetBundleManifest 객체를 불러오기 위해서 다음과 같은 코드를 사용하라고 섹션 6에서 언급하고 있다 :
경험해본 바에 의하면 저 코드를 이용해서 매니페스트 파일을 불러올 수 있는 것은 사실이다. 하지만 매니페스트 파일을 사용하기 위해 처음으로 유니티 5.6 문서를 본 개발자는 저 manifsetFilePath에 대해서 정확하게 어떤 경로를 의미하는지에 대한 난감함을 느끼게 될 것이다. 작성자 역시 같은 난감함을 느꼈고 이 manifestFilePath를 알아내기 위해 여러가지 시도를 해야봐야 했다.
위의 세 가지 방법 이 외에도 LoadAsset을 하는 방법을 바꾸어 보기도 했고 LoadFromFile 대신 LoadFromMemory나 비동기 함수를 사용하는 방법도 사용해 봤었고, 에셋 번들을 빌드한 이후에 나온 매니페스트 파일을 각 에셋 번들에 포함시킨 후에 재빌드하는 방법도 사용해 봤다. 간단히 결론만 말하면 그 모든 방법은 실패했다.
그리고 마지막 방법으로 시도해본 것이 AssetBundles.unity3d라는 파일을 호출하는 것이었다. 유니티에서 에셋 번들을 빌드하면 에디터에서 지정한 에셋 번들 이름을 가진 에셋 번들들과 그 매니페스트 파일이 생성되고, 그 외에 직접 지정한 이름이 아닌 AssetBundles라는 파일과 AssetBundles.manifest라는 파일이 생성되는데, 유니티 5.6 문서에는 이 파일의 역할이 무엇인지 명시되어 있지 않았다.
유니티 문서에서는 AssetBundleManifest를 호출하는 방법은 정확히 알려주었으나, 어떤 파일을 불러와야 되는지는 제대로 알려주지 않은 것이다. 구글링한 내용들에서도 "AssetBundles" 파일에 대한 언급은 없었기 때문에 정확한 해결법을 찾는데 더 어려웠던 것 같다.
1. ".unity3d"라는 확장자는 웹 서버를 통해 다운받은 에셋 번들을 로컬 저장소에 저장할 때 직접 지정한 확장자이다.
2. 에셋 번들을 빌드하면 확장자 없이 에셋 번들 이름만 적힌 파일이 나온다.
[유니티 어필리에이트 프로그램]
아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.
유니티(Unity)에서 씬(Scene) 역시 에셋 번들(Asset Bundle)에 포함 될 수 있다. 그 기본적인 방법은 유니티 5.6 문서의 섹션 7에 나와있다. 하지만 그 방법은 에셋 번들 매니저(Asset Bundle Manager)를 이용해야 하는 방법이고 너무 대략적인 내용이라 따라 실행하는 데에만 많은 시행 착오를 겪게 된다. 이 문서에서는 그에 비해 비교적 간단하고 즉시 활용할 수 있는 코드를 보여주고자 한다.
다음의 내용이 알려주고자 하는 코드의 전체이다 :
using System.IO; using System.Collections; using UnityEngine; using UnityEngine.SceneManagement;
if (isAdditive) loadMode = LoadSceneMode.Additive;
else loadMode = LoadSceneMode.Single;
SceneManager.LoadScene(loadScenePath, loadMode);
}
}
위의 코드를 활용하면 에셋 번들에 포함된 씬을 불러오는 것이 가능해진다. 다만, 게임 씬 에셋 번들이 다른 에셋 번들에 대한 종속성(Dependencies)를 가지고 있다면 씬을 불러오기 이전에 종속성을 가지고 있는 에셋 번들들을 먼저 불러와야만 한다. 만약 그렇게 하지 않으면, 불러오지 못한 오브젝트들은 게임 씬에서 missing으로 처리되어 빈 오브젝트로 나타나게 될 것이다.
예시로 보여준 코드에서는 게임 씬 에셋 번들을 불러오고 곧바로 에셋 번들 내에 존재하는 씬의 경로를 모두 탐색해서 씬을 불러왔지만 다른 방법으로는 게임이 시작되었을 때 게임 씬 에셋 번들을 불러와서 게임 씬들의 목록을 구성한 뒤에 필요할 때마다 활용하는 방식으로 사용할 수도 있다.
[유니티 어필리에이트 프로그램]
아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.
UnityWebRequest를 이용해서 원격 서버에서 받아온 에셋 번들을 로컬 저장소에 저장하는 방법
유니티 5.6 문서에서는 알려주지 않는 로컬 저장소 저장 방법
유니티 5.6 문서에서는 웹 서버에서 에셋 번들을 받아올 때 WWW.LoadFromCacheOrDownload 대신에 UnityWebRequest와 DownloadHandlerAssetBundle을 사용할 것을 권장하고 있다. 아래의 코드가 유니티 5.6 문서에서 보여주는 웹 서버에서 에셋 번들을 받아와서 메모리에 로드하는 예제이다.
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
public class UnityWebRequestExample : MonoBehaviour
{
IEnumerator InstantiateObject()
{
string uri = "file:///" + Application.dataPath + "/AssetBundles/" + assetBundleName;
UnityEngine.Networking.UnityWebRequest request = UnityEngine.Networking.UnityWebRequest.GetAssetBundle(uri, 0);
yield return request.Send();
AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);
GameObject cube = bundle.LoadAsset<GameObject>("Cube");
GameObject sprite = bundle.LoadAsset<GameObject>("Sprite");
Instantiate(cube);
Instantiate(sprite);
}
}
서버에서 받아온 에셋 번들을 메모리에 넣어두고 사용하고자 하는 목적이 아니라 받아온 에셋 번들을 클라이언트의 로컬 저장소에 저장하고 패치하는 시스템을 만들고자할 때에는 부적절한 예제이며, 로컬 저장소에 저장하는 방법은 유니티 5.6의 문서에서는 알려주지 않는다.
에셋 번들을 로컬 저장소에 저장하는 방법
서버에서 받아온 에셋 번들을 로컬 저장소에 저장하려면 받아온 데이터를 파일 입출력을 해야했는데, 그러기 위해서는 우선 받아온 에셋 번들의 데이터, byte[] data를 찾아내고 거기에 엑세스할 수 있어야 했다. 그래서 첫 번째로 시도한 방법이 위의 예제에서 UnitWebRequest.GetAssetBundle()로 받아온 UnityWebRequest request에서 request.downloadHandler.data를 통해서 접근하는 것이었다.
using System.IO; using System.Collections; using UnityEngine; using UnityEngine.Networking;
public class AssetLoader : MonoBehaviour { public string[] assetBundleNames;
하지만 위의 방법은 에셋 번들에 대한 원시 데이터 접근은 지원하지 않는다는 예외를 발생시키고 실패한다. 즉, GetAssetBundle로 웹 서버에서 불러온 데이터는 데이터에 대한 직접 접근을 허용하지 않으니 파일 입출력을 통해서 로컬 저장소에 저장할 수 없다는 것이다.
그렇기 때문에 로컬 저장소에 저장하기 위해서는 웹 서버에서 에셋 번들을 받아올 때 다른 방법을 취해야 했다. 아래의 예제가 다른 방식으로 웹 서버에서 에셋 번들을 받아와서 로컬 저장소에 저장하는 예제이다 :
using System.IO; using System.Collections; using UnityEngine; using UnityEngine.Networking;
public class AssetLoader : MonoBehaviour {
// 서버에서 받아오고자 하는 에셋 번들의 이름 목록
// 지금은 간단한 배열 형태를 사용하고 있지만 이후에는
// xml이나 json을 사용하여 현재 가지고 있는 에셋 번들의 버전을 함께 넣어주고
// 서버의 에셋 번들 버전 정보를 비교해서 받아오는 것이 좋다. public string[] assetBundleNames;
IEnumerator SaveAssetBundleOnDisk() {
// 에셋 번들을 받아오고자하는 서버의 주소
// 지금은 주소와 에셋 번들 이름을 함께 묶어 두었지만
// 주소 + 에셋 번들 이름 형태를 띄는 것이 좋다. string uri = "http://127.0.0.1/character";
// 웹 서버에 요청을 생성한다. UnityWebRequest request = UnityWebRequest.Get(uri); yield return request.Send();
// 에셋 번들을 저장할 경로
string assetBundleDirectory = "Assets/AssetBundles"; // 에셋 번들을 저장할 경로의 폴더가 존재하지 않는다면 생성시킨다. if (!Directory.Exists(assetBundleDirectory)) { Directory.CreateDirectory(assetBundleDirectory); }
// 파일 입출력을 통해 받아온 에셋을 저장하는 과정 FileStream fs = new FileStream(assetBundleDirectory + "/" + "character.unity3d", System.IO.FileMode.Create); fs.Write(request.downloadHandler.data, 0, (int)request.downloadedBytes); fs.Close(); } }
위 예제에서 보다시피 UnityWebRequest.Get() API를 사용했을 경우 받아온 에셋 번들의 원시 데이터에 대한 접근이 가능해진다. 이로 인해서 파일 입출력을 통한 원격 서버에서 받아온 에셋 번들의 로컬 저장소 저장에 성공하게 되었다.
파일 입출력을 통한 저장 방법
받아온 에셋 번들을 파일 입출력을 통해 로컬 저장소에 저장하는 방법은 여러가지가 있다. 적당하다고 생각되는 방법을 골라서 사용하면 될 것이다.
// 파일 저장 방법 1 FileStream fs = new FileStream(assetBundleDirectory + "/" + "character.unity3d", System.IO.FileMode.Create); fs.Write(request.downloadHandler.data, 0, (int)request.downloadedBytes); fs.Close();
// 파일 저장 방법 2 File.WriteAllBytes(assetBundleDirectory + "/" + "character.unity3d", request.downloadHandler.data);
// 파일 저장 방법 3 for (ulong i = 0; i < request.downloadedBytes; i++) { fs.WriteByte(request.downloadHandler.data[i]); // 저장 진척도 표시
}
로컬 저장소에 저장한 에셋 번들을 불러와서 사용하는 방법
위의 과정을 통해 원격 서버에서 받아온 에셋 번들을 로컬 저장소에 저장하는데 성공했다. 이 다음에 해야할 작업은 저장한 에셋 번들을 불러와서 사용하는 것이다. 그 작업은 유니티 5.6 문서에 나오는 기본 예제를 구현하는 것으로 충분히 가능하다.
using System.IO; using System.Collections; using UnityEngine; using UnityEngine.Networking;
public class AssetLoader : MonoBehaviour { IEnumerator LoadAssetFromLocalDisk() { string assetBundleDirectory = "Assets/AssetBundles"; // 저장한 에셋 번들로부터 에셋 불러오기 var myLoadedAssetBundle = AssetBundle.LoadFromFile(Path.Combine(assetBundleDirectory + "/", "character.unity3d")); if (myLoadedAssetBundle == null) { Debug.Log("Failed to load AssetBundle!"); yield break; } else Debug.Log("Successed to load AssetBundle!");
var prefab = myLoadedAssetBundle.LoadAsset<GameObject>("P_C0001"); Instantiate(prefab, Vector3.zero, Quaternion.identity); } }
[유니티 어필리에이트 프로그램]
아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.
이번 섹션에서는 프로젝트에서 에셋 번들을 사용할 때 일반적으로 발생하는 몇 가지 문제에 대해서 설명할 것이다.
에셋 복제(Asset Duplication) - 유니티 5의 에셋 번들 시스템은 오브젝트가 에셋 번들에 내장 되었을 때 객체의 모든 종속성을 발견하게 된다. 이것은 에셋 데이터베이스를 통해서 수행된다. 이 종속성 정보는 에셋 번들에 포함된 오브젝트 집합을 결정하는데 사용된다.
에셋 번들에 명시적으로 등록된 객체는 해당 에셋 번들에만 등록된다. 오브젝트의 에셋 임포터의 assetBundleName 속성이 비어있지 않은 문자열로 설정된 경우에만 객체가 "명시적으로 등록"되는 것이다.
에셋 번들에 명시적으로 등록되지 않은 오브젝트는 이 것을 참조하는 오브젝트가 포함된 모든 에셋 번들에 등록된다.
두 개의 서로 다른 오브젝트가 서로 다른 에셋 번들에 등록되어 있는데, 둘 다 하나의 객체를 참조하고 있다면 그 객체는 두 에셋 번들에 모두 복사된다. 이렇게 중복된 종속성 역시 인스턴스화 되는데, 이러한 인스턴스는 하나의 객체가 아니라 달라진 식별자로 인해 서로 다른 객체로 간주된다. 이로 인해서 응용 프로그램의 에셋 번들의 전체 크기가 증가하게 된다. 또한 응용 프로그램에서 부모를 모두 불러오는 경우 두 개의 오브젝트 사본이 메모리에 불러와진다.
이 문제를 해결할 수 있는 몇 가지 방법이 있는데 그것은 다음과 같다 :
1. 서로 다른 에셋 번들에 등록된 오브젝트들이 종속성을 공유하지 않도록해야 한다. 종속성을 공유하는 모든 오브젝트는 동일한 에셋 번들에 배치함으로써 종속성의 복제를 배제할 수 있다.
- 이 방법은 일반적으로 공유 종속성이 많은 프로젝트에서는 사용하기 부적절하다. 편리하고 효율적으로 만들기 위해서 과도하게 자주 다시 빌드하고 다시 다운로드해야 하는 한덩어리의 에셋 번들이 생성될 것이다.
2. 종속성을 공유하는 2개의 에셋 번들이 동시에 불러와지지 않도록 에셋 번들을 세그먼트화해야 한다.
- 이 방법은 레벨 기반 게임과 같은 특정 유형의 프로젝트에 적용할 수 있다. 하지만 여전히 프로젝트의 에셋 번들의 크기가 불필요하게 증가하고 빌드 시간과 로딩 시간이 증가하는 문제가 있다.
3. 모든 종속성 에셋이 자체 에셋 번들에 빌드되었는지 확인해야 한다. 이 방식은 에셋의 복제 위험을 완전히 제거하지만 복잡성 문제를 발생시킨다. 이것을 위해서 응용 프로그램은 에셋 번들 간의 종속성을 추적하고 AssetBundle.LoadAsset API를 호출하기 전에 올바른 에셋 번들이 불러와졌는지 확인해야 한다.
유니티 5에서, 오브젝트 의존성은 UnityEditor 네임 스페이스의 AssetDatabase API를 통해서 추적된다. 네임 스페이스의 이름이 의미하듯이 이 API는 유니티 에디터 상에서만 사용할 수 있으며 런타임 시에는 사용할 수 없다. AssetDatabase.GetDependencies는 특정 오브젝트나 에셋의 모든 즉각적인 종속성을 찾는데 사용할 수 있다. 참고로 이러한 종속성에는 자체 종속성이 있을 수 있다. 추가적으로 AssetImporter API를 사용하여 특정 오브젝트가 등록되어 있는 에셋 번들을 쿼리할 수 있다.
AssetDatabase와 AssetImporter API를 결합하여 모든 에셋 번들의 직접 또는 간접 종속성이 에셋 번들에 등록되도록 하거나, 두 개의 에셋 번들이 에셋 번들에 등록되지 않은 종속성을 공유하지 않도록하는 에디터 스크립트를 작성할 수 있다. 에셋을 복제하는데 소모되는 메모리 비용을 생각하면 모든 프로젝트에 이러한 스크립트가 있는 것이 좋다.
스프라이트 아틀라스 복제(Sprite Atlas Duplication) - 이 섹션에서는 자동으로 생성된 스프라이트 아틀라스와 함께 사용될 때 발생하는 유니티 5의 에셋 종속성 계산 코드의 단점에 대해서 설명한다.
자동으로 생성된 스프라이트 아틀라스느느 스프라이트 아틀라스가 생성된 스프라이트 오브젝트를 포함하는 에셋 번들에 등록된다. 만약 스프라이트 오브젝트가 여러 에셋 번들에 등록되어 있다면 스프라이트 아틀라스는 하나의 에셋 번들에 등록되지 않고 복제된다. 스프라이트 오브젝트가 에셋 번들에 등록되지 않은 경우라면 스프라이트 아틀라스는 에셋 번들에도 등록되지 않는다.
스프라이트 아틀라스가 중복되지 않도록 하려면 동일한 스프라이트 아틀라스에 태그가 지정된 모든 스프라이트가 하나의 에셋 번들에 등록되어 있는지 확인해야 한다.
유니티 5.2.2p3 이전 버전
자동으로 생성된 스프라이트 아틀라스는 에셋 번들에 등록되지 않는다. 그렇기 때문에 구성 스프라이트를 포함하는 모든 에셋 번들 및 해당 구성 스프라이트를 참조하는 모든 에셋 번들에 포함된다. 이 문제 때문에 유니티 스프라이트 패커를 사용하는 모든 유니티 5 프로젝트에 대해서 5.2.2p4, 5.3 또는 최신 버전의 유니티로 업드레이드하는 것을 강력히 권장한다.
만약 업그레이드를 할 수 없는 상황에 놓인 프로젝트의 경우에 이 문제에 대한 두 가지 해결 방법이 있다.
1. 쉬운 방법 : 유니티의 내장 스프라이트 패커의 사용을 피할 것. 외부 도구에 의해 생성된 스프라이트 아틀라스는 정상적인 에셋이 될 것이고, 에셋 번들에 적절하게 등록될 수 있다.
2. 어려운 방법 : 자동으로 아틀라스화된 스프라이트를 사용하는 모든 오브젝트를 스프라이트와 동일한 에셋 번들에 등록하라.
- 이렇게 하면 생성도니 스프라이트 아틀라스가 다른 에셋 번들의 간접적인 종속성으로 보이지 않으며 복제되지 않게 된다.
- 이 해결책은 유니티의 스프라이트 패커를 사용하는 워크 플로우를 보존하지만 에셋을 다른 에셋 번들로 분리하는 개발자의 능력을 저하시키며, 아틀라스를 참조하는 컴포넌트에서 데이터가 변경될 때마다 전체 스프라이트 어트리뷰트의 재 다운로드를 강제한다. 아틀라스 자체는 변경되지 않는다.
안드로이드 텍스처(Android Texture) - 안드로이드 환경은 기기 별로 매우 세분화되어 있기 때문에, 텍스처를 여러 가지 형식으로 압축해야하는 경우가 있다. 모든 안드로이드 기기가 ETC1을 지원하지만 ETC1은 알파 채널이 있는 텍스처를 지원하지 않는다. 만약 응용 프로그램이 OpenGL ES2 지원이 필요하지 않다면, 이 문제를 해결하는 가장 깔끔한 방법은 모든 안드로이드 OpenGL ES3 장치에서 지원되는 ETC2를 사용하는 것이다.
대부분의 응용 프로그램은 ETC2 지원을 사용할 수 없는 구형 기기에서 제공되어야 한다. 이 문제를 해결하는 한 가지 방법은 유니티 5의 에셋 번들 Variants를 사용하는 것이다(다른 옵션에 대한 자세한 내용은 유니티 안드로이드 최적화 가이드를 참조).
에셋 번들 Variants를 사용하려면 ETC1을 사용하여 깨끗하게 압축할 수 없는 모든 텍스처를 텍스처 전용 에셋 번들로 분리해야 한다. 그 다음 DXT5, PVRTC, ATITC와 같은 공급 업체별 텍스처 압축 형식을 사용하여 안드로이드 환경 별 비-ETC-가능 슬라이스를 지원하기 위해 이러한 에셋 번들의 충분한 variants를 만들어야 한다. 각 에셋 번들 Variants에 대해 포함된 텍스처의 텍스처 임포터 설정을 Variants에 적합한 압축 포맷으로 변경한다.
런타임 시에, SystemInfo.SupportsTextureFormat API를 사용하여 다양한 텍스처 압축 형식에 대한 지원을 감지할 수 있다. 이 정보는 지우너되는 형식으로 압축된 텍스처가 포함된 에셋 번들 Variants를 선택하고 불러오는데 사용해야 한다.
iOS 파일 처리 과용(iOS File Handle Overuse) - 이 섹션에서 설명하는 문제는 유니티 5.3.2p2에서 수정되었다. 현재 버전의 유니티는 이 문제의 영향을 받지 않는다.
유니티 5.3.2p2 이전 버전에서는 유니티가 에셋 번들이 불러와지는 전체 시간동안 에셋 번들에 대한 열린 파일 핸들을 보유한다. 이것은 사실 대부분의 플랫폼에서는 문제가 되지 않지만, iOS는 프로세스가 동시에 열 수 있는 파일의 핸들 수를 255개까지만 제한하기 때문에 이 한도를 초과하여 에셋 번들을 불러오면 로딩 호출이 "Too Many Open File Handles" 오류와 함께 실패한다.
이것은 수백 또는 수천개의 에셋 번들에서 콘텐츠를 나누려는 프로젝트에서 발생하는 일반적인 문제였다.
새로운 버전의 유니티로 업그레이드할 수 없는 프로젝트에 대한 임시 해결책은 다음과 같다.
1. 관련된 에셋 번들을 합쳐서 사용중인 에셋 번들의 수를 줄인다.
2. AssetBundle.Unload(false)를 사용하여 에셋 번들의 파일 핸들을 닫고 로드된 오브젝트의 라이프 사이클을 수동으로 관리한다.
[유니티 어필리에이트 프로그램]
아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.
에셋 번들 패치는 새로운 에셋 번들을 다운로드하고 기존 에셋 번들을 교체하는 것처럼 간단하다. WWW.LoadFromCasheOrDownload나 UnityWebRequest를 사용하여 응용 프로그램의 캐시된 에셋 번들을 관리하는 경우 선택한 API의 매개변수에 다른 버전을 전달하면 새 에셋번들이 다운로드 된다.
패치 시스템에서 해결해야할 더 큰 문제는 대체할 에셋 번들을 찾아내는 것이다. 패치 시스템에는 2가지의 정보 목록이 필요하다 :
- 현재 다운로드 되어 있는 에셋 번들의 목록과 그 버전의 정보
- 서버에 올라가 있는 에셋 번들의 목록과 그 버전의 정보
패치 시스템은 서버 측 에셋 번들의 목록을 다운로드하고 현재 다운로드되어 있는 에셋 번들의 목록과 비교해야 한다. 누락된 에셋 번들이나 버전 정보가 변경된 에셋 번들은 다시 다운로드해야 한다.
에셋 번들의 변경 사항을 찾아내는 사용자 정의 시스템을 제작할 수도 있다. 자체적인 시스템을 제작하는 대부분의 개발자는 에셋 번들 파일 정보 목록에 JSON과 같은 업계 표준 데이터 형식을 사용하고 MD5와 같은 체크섬 계산을 하는 표준 C# 클래스를 사용하는 것을 선택한다.
유니티는 데이터를 결정적 방식(deterministic manner)으로 정렬하여 에셋 번들을 빌드한다. 그렇기 때문에 커스텀 다운로더가 있는 응용 프로그램에서 차등 패치 시스템을 구현할 수 있다.
유니티는 차등 패치를 위한 기본 메커니즘을 제공하지 않으며 WWW.LoadFromCacheOrDownload나 UnityWebRequest도 기본 제공 캐싱 시스템을 사용할 때 차등 패치를 수행하지 않는다. 차등 패치가 필요한 경우에는 커스텀 다운로더를 직접 제작해야 한다.
[유니티 어필리에이트 프로그램]
아래의 링크를 통해 에셋을 구매하시거나 유니티를 구독하시면 수익의 일부가 베르에게 수수료로 지급되어 채널의 운영에 도움이 됩니다.
이 함수는 에셋 번들 데이터가 들어있는 바이트 형식의 배열을 가져온다. (선택사항)원한다면 CRC 값을 전달할 수 있다. 에셋 번들이 LZMA 방식으로 압축되어 있다면 번들을 로딩하는 동안 에셋 번들을 압축 해체해야 한다. LZ4 방식으로 압축된 에셋 번들은 압축되어 있는 상태에서도 로드가 가능하다.
다음은 이 메서드를 사용하는 방법의 예시이다.
using System.IO; using UnityEngine; public class AssetBundleLoadExample : MonoBehaviour { IEnumerator LoadFromMemoryAsync(string path) { AssetBundleCreateRequest createRequest = AssetBundle.LoadFromMemoryAsync(File.ReadAllBytes(path)); yield return createRequest; AssetBundle bundle = createRequest.assetBundle; var prefab = bundle.LoadAsset<GameObject>("MyObject");
Instantiate(prefab); } }
하지만 이것이 LoadFromMemoryAsync를 사용하는 유일한 방법은 아니다. File.ReadAllByte(path)는 바이트 배열을 얻은 임의의 절차로 대체될 수 있다.
이 API는 로컬 저장소에서 압축되지 않은 에셋 번들을 로드할 때 매우 효율적이다. LoadFromFile은 압축되지 않았거나 청크 기반(LZ4)으로 압축된 번들의 경우 디스크로부터 직접 에셋 번들을 로드해 온다. 이 메서드로 완적히 압축된(LZMA) 에셋 번들을 로드하면 먼저 메모리에 올리기 전에 압축을 해제한다.
다음은 LoadFromFile을 사용하는 예제이다 :
using System.IO; using UnityEngine;
public class LoadFromFileExample : MonoBehaviour { void Start() { var myLoadedAssetBundle = AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, "myassetBundle")); if (myLoadedAssetBundle == null) { Debug.Log("Failed to load AssetBundle!"); return; } var prefab = myLoadedAssetBundle.LoadAsset<GameObject>("MyObject"); Instantiate(prefab); } }
참고 :: 유니티 5.3 이하의 안드로이드 기기에서는 스트리밍 에셋 경로에서 에셋번들을 로드하려고 할때 이 API가 실패한다. 이는 해당 경로에 압축된 .jar 파일 내에 존재하기 때문이다. 유니티 5.4 이상에서는 Streaming Assets과 함께 이 API 호출을 사용할 수 있다.
이 API의 사용은 권장하지 않는다.(UnityWebRequest를 사용하라) 이 API는 원격 서버에서 에셋 번들을 다운로드하거나 로컬 에셋 번들을 로드하는데 유용하다. 이것은 구 버전의 API로 UnityWebRequest API를 사용할 것을 권장한다.
원격 위치에서 에셋 번들을 로드하면 에셋 번들이 자동으로 캐시된다. 만약 에셋 번들이 압축된 상태라면 작업 스레드에서 에셋 번들의 압축을 해제하여 캐시에 등록한다. 압축이 해제되고 캐시된 에셋 번들은 AssetBundle.LoadFromFile과 똑같이 로드된다.
다음은 LoadFromCacheOrDownload를 사용하는 예제이다.
using UnityEngine; using System.Collections;
public class LoadFromCacheOrDownloadExample : MonoBehaviour { IEnumerator Start() { while (!Caching.ready) yield return null;
var www = WWW.LoadFromCacheOrDownload("http://myserver.com/myassetBundle.unity3d", 5); yield return www; if (!string.IsNullOrEmpty(www.error)) { Debug.Log(www.error); yield return null; } var myLoadedAssetBundle = www.assetBundle;
var asset = myLoadedAssetBundle.mainAsset; } }
AssetBundle의 바이트를 WWW 객체에 캐싱하는 메모리 오버헤드가 발생하기 때문에 WWW.LoadFromCacheOrDownload를 사용하는 모든 개발자는 AssetBundle의 크기가 몇 메가(원문에서는 a few megabytes) 수준을 넘지 않게 유지하는 것이 좋다. 또한 모바일 기기와 같이 메모리가 제한된 플랫폼에서 작업하는 개발자는 메모리 스파이크를 피하기 위해 코드가 한 번에 하나의 에셋 번들만 다운로드하도록 하는 것이 좋다.
만약 캐시 폴더에 추가 파일을 캐시할 여유 공간이 없다면, LoadFromCacheOrDownload는 새 에셋 번들을 저장할 충분한 공각이 확보될 때까지 캐시에서 사용된 시점이 가장 오래된 에셋 번들을 삭제할 것이다. 더 이상 공간을 만들 수 없는 경우(하드 디스크가 가득 찼거나 캐시의 모든 파일이 현재 사용 중인 경우), LoadFromCacheOrDownload는 캐싱을 하지 않고 파일을 메모리에 스트리밍할 것이다.
LoadFromCacheOrDownload를 강제하려면 version 매개변수(두 번째 매개변수)를 변경해야 한다. 에셋 번들은 함수로 전달된 버전이 현재 캐시된 에셋 번들의 버전과 일치하는 경우에만 캐시에서 로드된다.
UnityWebRequest는 에셋 번들을 처리하기 위한 특정 API를 가지고 있다. 먼저 UnityWebRequest.GetAssetBundle을 사용하여 웹 요청을 생성하면, 요청을 반환한 후 요청 객체를 DownloadHandlerAssetBundle.GetContent(UnityWebRequest)로 전달한다. 이 GetContent함수를 호출하면 AssetBundle 객체를 반환한다.
AssetBundle.LoadFromFile와 같은 효율성으로 AssetBundle을 로드하기 위해 에셋 번들을 다운로드한 이후에 DownloadHandlerAssetBundle 클래스에서 assetBundle 속성을 사용할 수도 있다.
아래의 예제는 두 개의 GameObject가 포함된 에셋 번들을 로드하고 인스턴스화(instantiate)하는 방법을 보여준다.
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
public class UnityWebRequestExample : MonoBehaviour
{
IEnumerator InstantiateObject()
{
string uri = "file:///" + Application.dataPath + "/AssetBundles/" + assetBundleName;
UnityEngine.Networking.UnityWebRequest request = UnityEngine.Networking.UnityWebRequest.GetAssetBundle(uri, 0);
yield return request.Send();
AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);
GameObject cube = bundle.LoadAsset<GameObject>("Cube");
GameObject sprite = bundle.LoadAsset<GameObject>("Sprite");
Instantiate(cube);
Instantiate(sprite);
}
}
UnityWebRequest를 사용하면 개발자가 다운로드한 데이터를 보다 유연하게 처리하고 불필요한 메모리 사용을 없앨 수 있다는 이점이 있다. 이것은 UnityEngine.WWW 클래스보다 최신의 API이다.
에셋 번들에서 에셋 불러오기
에셋 번들을 성공적으로 다운로드했으므로, 원하는 에셋을 불러올 차례이다.
제네릭 코드의 일부분 :
T objectFromBundle = bundleObject.LoadAsset<T>(assetName);
여기서 T는 불러오려는 에셋의 타입이다.
에셋을 불러오는 방법을 결정하는 몇 가지 옵션이 있는데 LoadAsset, LoadAllAsset과 각각에 대응되는 비동기 방식인 LoadAssetAsync, LoadAllAssetAsync가 그것이다.
위의 예제에서 매니페스트 객체를 통해 AssetBundleManifest API 호출에 액세스할 수 있다. 여기에서 매니페스트를 사용하면 만든 에셋 번들에 대한 정보를 얻을 수 있다. 이 정보에는 종속성 데이터, 해시 데이터 및 에셋 번들의 배리언트(Variant, 변형) 데이터가 포함된다.
이전 섹션에서 에셋 번들의 종속성에 대해서 설명했듯이, 에셋 번들이 다른 에셋 번들에 종속성을 가지고 있다면 원래의 번들에서 에셋을 불러오기 전에 해당 번들을 불러와야 한다. 매니페스트 객체는 로딩 종속성을 동적으로 찾을 수 있도록 해준다. "assetBundle"이라는 이름의 에셋 번들에 대한 모든 종속성을 불러오려고 한다고 가정해보자.
유니티는 활성화된 씬에서 오브젝트가 제거되었을때 오브젝트를 자동으로 언로드(Unload)하지 않는다. 에셋 정리는 특정 시간에 자동적으로 이루어지며, 수동으로도 실행될 수 있다.
에셋 번들을 로드하고 언로드해야할 때를 아는 것이 중요하다. 에셋 번들을 잘못 언로드하면 메모리 상에서의 오브젝트와 텍스처 누락등의 바람직하지 않은 상황이 발생할 수 있다.
에셋 번들 관리에 있어서 이해해야할 가장 중요한 점은 AssetBundle.Unload(bool) 함수를 언제 호출해야 하는가와 함수의 매개변수에 true 혹은 false 어떤 인자를 전달해야 하는가이다. Unload는 에셋 번들을 언로드하는 비정적(non-static)함수이다. 이 API는 호출중인 에셋 번들의 헤더 정보를 언로드 한다. 이 매개변수는 이 에셋 번들로부터 인스턴스화된 모든 오브젝트를 언로드할지에 대한 여부를 나타낸다.
에셋 번들로부터 불러온 오브젝트에 대해 AssetBunble.Unload(true)를 사용했는데, 만약 이 오브젝트가 현재 활성화된 씬에서 사용중이라면, 이것은 앞서 이야기 한것처럼 텍스처 누락의 원인이 될 수 있다.
아래와 같이 머티리얼 M이 에셋 번들 AB에서 로드되었다고 가정해보자.
만약 AB.Unload(true)가 호출된다면, 활성화된 씬에서 M의 모든 인스턴스는 언로드되고 파괴된다.
대신에 AB.Unload(false)를 호출하면 M과 AB의 현재 인스턴스 연결이 끊어진다.
만약 AB.LoadAsset()을 호출하여 AB를 다시 로드한다고 하여도, 유니티는 새로 로드된 머티리얼에 기존에 존재하는 복사본 M을 연결시켜주지 않는다. 그 대신에 M의 2개의 복사본이 로드된다.
일반적으로 AssetBundle.Unload(false)를 사용하면 좋은 상황이 발생하지 않는다. 대부분의 프로젝트는 AssetBundle.Unload(true)를 사용하여 오브젝트를 메모리에 복제하지 않아야한다.
대부분의 프로젝트는 AssetBundle.Unload(true)를 사용하고 오브젝트가 중복되지 않도록 보장하는 메서드를 채택해야한다. 두 가지 일반적인 방법은 다음과 같다 :
- 응용 프로그램의 실행 시간 동안에 발생하는 레벨 사이 혹은 로딩 화면과 같이 짧은 틈 사이에 에셋 번들이 언로드되는 지점을 정의한다.
- 개별의 오브젝트에 대해 참조 카운트를 유지하고 모든 구성 오브젝트가 사용되지 않는 경우에만 에셋 번들을 언로드한다. 이 방법을 사용하면 응용 프로그램이 메모리를 복제하지 않고 개별 오브젝트를 언로드하고 다시 로드할 수 있다.
응용 프로그램에서 AssetBundle.Unload(false)를 반드시 사용해야만 한다면, 개별 오브젝트는 다음의 2가지 방법으로만 언로드해야 한다.