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);
}
}


출처: http://wergia.tistory.com/32 [베르의 프로그래밍 노트]
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);
}
}


출처: http://wergia.tistory.com/32 [베르의 프로그래밍 노트]
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);
}
}


출처: http://wergia.tistory.com/32 [베르의 프로그래밍 노트]
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);
}
}


출처: http://wergia.tistory.com/32 [베르의 프로그래밍 노트]
using UnityEngine;
using UnityEngine.Networking;

public class UnityWebRequestExample : MonoBehaviour
{
IEnumerator InstantiateObject()
{
string uri = "file:///" + Application.dataPath + "/AssetBundles/" + assetBundleName;
UnityWebRequest request = 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);
}
}
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);
}
}


출처: http://wergia.tistory.com/32 [베르의 프로그래밍 노트]

서버에서 받아온 에셋 번들을 메모리에 넣어두고 사용하고자 하는 목적이 아니라 받아온 에셋 번들을 클라이언트의 로컬 저장소에 저장하고 패치하는 시스템을 만들고자할 때에는 부적절한 예제이며, 로컬 저장소에 저장하는 방법은 유니티 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;

IEnumerator AccessFailMethod()
{

string uri = "http://127.0.0.1/character";


UnityWebRequest request = UnityWebRequest.GetAssetBundle(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();
}
}

하지만 위의 방법은 에셋 번들에 대한 원시 데이터 접근은 지원하지 않는다는 예외를 발생시키고 실패한다. 즉, 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);
}
}


  1. blueasa 2017.05.10 20:15 신고

    좋은 정보 감사합니다. :)

    • wergia 2017.05.10 21:48 신고

      방문 감사드립니다. 올린 글이 도움이 되셨으면 좋겠네요 ^^

  2. Vector Space 2017.05.18 14:49 신고

    UnityWebRequest에서도 버전 번호나 Hash128을 입력하면 LoadFromCacheOrDownload와 동일하게 캐싱 영역에 저장이 되어
    한번 다운로드되면 다음에는 캐싱된 것을 읽습니다. 굳이 로컬저장소에 별도로 저장하시는 이유가 있나요?

    • wergia 2017.05.18 17:07 신고

      받아온 애셋번들을 일정한 경로에 저장해두고 서버와 클라이언트의 애셋번들이 버전이 일치하는지 확인하고 일치한다면 해당 애셋번들을 로드하고 일치하지 않는다면 새롭게 애셋번들을 받아서 로드하기 위해서 로컬저장소에 저장했습니다.

      제 짧은 지식으로는 UnityWebRequest를 통해서 웹 서버에서 에셋 번들을 받아오면 메모리에만 캐싱되고 실제 로컬 저장소에는 저장되지 않는 것으로 알고 있어서 매번 게임을 실행하게 될때마다 에셋번들들을 서버에서 받아와야하는 것으로 알고 있습니다. 소규모의 작은 애셋번들만 받아오면 되는 게임의 경우에는 문제가 없겠으나 대용량의 애셋번들을 사용하는 게임에 경우에는 애셋번들들을 로컬저장소에 저장해두고 버전이 바뀌는 번들들만 패치형식으로 바꾸어서 사용하기 위해서 이러한 방식을 사용했습니다.

  3. prhymery 2018.05.31 15:39

    올려주신 글 덕분에 큰 도움이 되었습니다.
    질문이 하나 있습니다.
    위에 설명해주신 방법으로
    에셋번들 빌드, 에셋번들 다운까지는 잘 됩니다.
    에셋번들 업로드는 구글 드라이브에 올렸고
    다운로드는 파일 공유주소를 이용해서 받았습니다.
    여기까지는 문제없이 되는데
    에셋번들을 로드하는 과정에서 에러가 자꾸 발생합니다.
    var myLoadedAssetBundle = AssetBundle.LoadFromFile(Path.Combine(assetBundleDirectory + "/", "character.unity3d"));
    위의 라인에서
    Unable to read header from archive file:
    위의 에러가 발생합니다.
    혹시 원인을 아신다면 알려주시면 감사하겠습니다.

    ps. 이게 원인인지는 모르겠으나 구글드라이브 웹사이트를 이용하여 받은 파일과 유니티 에디터를 이용해서 받은 파일의 용량 차이가 납니다.
    유니티 에디터에서 다운받아서 로드하면 에러가 나지만, 웹사이트를 이용하여 해당 폴더에 넣어서 로드하게되면 또 잘 됩니다.

    • wergia 2018.06.01 14:22 신고

      음 파일 크기가 바뀌어서 발생한 문제가 맞는것 같습니다. 에디터에서 구글 드라이브에서 다운받을때 어떤 방식을 사용하나요?

      구글에서 제공하는 api를 사용했다면 파일 받으면서 구글 api 쪽에서 덧붙인 헤더 같은게 파일 데이터에 들어가서 그렇게 된 것으로 추정됩니다.

    • prhymery 2018.06.01 15:30

      친절한 답변 감사합니다.
      구글 드라이브가 문제였습니다.
      다른 웹 호스팅을 이용해서 하니까 잘 되네요
      감사합니다.

  4. 코드농사 2018.06.21 16:07

    안녕하세요~

    덕분에 많은 걸 배우고 갑니다.

    마지막에 궁금한게 생겼습니다.

    에셋번들목록을 json, xml로 만든다고 하셨는데

    manifest파일로 번들목록을 대신 할 수 있지않을까요?(manifest.GetAllAssetBundles())

    만약 패치부분이 문제라면 내부에 hash정보도 있으므로 서버manifest파일과 클라캐쉬된manifest파일의 해쉬값비교로 패치하는 시스템을 구현 할 수 있을 것같은데

    혹시 다른 이유라도 있을까요??

    감사합니다.

    • wergia 2018.07.18 10:34 신고

      네 충분히 매니페스트 파일에 들어있는 CRC나 해시 값을 비교해서도 충분히 패치 시스템을 구현할 수 있습니다.

  5. 데브 2018.07.23 22:23

    안녕하세요

    좋은 글 감사합니다.

    혹시 저장 단계에서 프리팹이나 게임오브젝트로도 가능한지 여쭙고자 이렇게 글 남겨봅니다.

    혹시 가능하다면 방법이나 키워드를 부탁드려도 될까요?

    더운 여름 건강 조심하시고 즐거운 하루하루 되세요 ^^

    • wergia 2018.07.24 13:25 신고

      저장단계에서 프리팹니아 게임 오브젝트로 저장한다는게 애셋번들에서 프리팹이나 게임오브젝트로 추출해서 로컬에 저장한다는 뜻인가요?

      그런 방법은 생각해본적이 없어서 그런지, 잘 모르겠네요.

      도움을 못드려서 죄송합니다.

    • 데브 2018.07.25 16:49

      말씀 해주신 부분이 맞습니다 ^^

      제가 유별나게 생각을해서 고민스럽게 만들어 드렸네요 ㅠㅠ

      남겨주신 글들로 어셋번들 잘 구현하고 있습니다.

      감사합니다 ^^

  6. redblacktree 2018.07.26 15:02

    혹시 에셋번들을 로컬에 저장한뒤에 삭제하는 방법이 따로 존재하나요?

    • wergia 2018.08.20 16:23 신고

      일반 파일 삭제하듯이 삭제하는 방법도 있고 덮어쓰기도 가능합니다.

+ Recent posts