유니티 공식 메뉴얼 에 있는 에셋 번들에 대한 설명 사진이다.
리소스를 에셋 번들이라는 이름으로 묶어서 외부로 뺄 수 있는 기능으로
특히나 모바일 게임에서 apk 자체의 용량을 줄이기 위해 매우 유용하게 사용된다.
데스크탑의 경우에도 예외는 아니며 버그 조금 수정했다고 전체 게임을 다시 받게 할 수는 없는 노릇이기에
이미지, 사운드, 모델 등으로 분류를 해서 번들로 관리를 한다면
필요한 리소스들만 버전 체크로 업데이트해서 유저, 개발자 서로 편할 수 있다.
또한 DownLoadable Content 와 같은 기능을 구현할 때도 매우 유용하게 사용할 수 있다.
이제 이 좋은 기능을 한번 사용해보자.
우선 에셋 번들을 에셋에 설정하는 법은 간단하다.
옆 사진과 같이 인스펙터 하단의 AssetBundle 에서
New 를 눌러서 새로 추가해주면 된다.
리소스 자체 또는 폴더에 설정이 가능하며
폴더에 지정할 경우 하위 파일이 전부 포함된다.
또한 위와 같이 / 를 추가할 경우
자동으로 트리구조로 구성할 수 있으며
번들을 추출할 때도 폴더가 생성된다.
에셋을 지정했으니 이제 서버로 올릴 형태로 빌드를 해줘야 한다.
using System.IO;
using UnityEditor;
public class CreateAssetBundles
{
[MenuItem("Assets/Build AssetBundles")]
static void BuildAllAssetBundles()
{
string dir = "Assets/AssetBundles";
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
BuildPipeline.BuildAssetBundles(dir, BuildAssetBundleOptions.None, EditorUserBuildSettings.activeBuildTarget);
}
}
에디터 스크립트로 Editor 폴더에 넣고
[MenuItem] 의 메뉴창에서 빌드를 하면 dir 경로에 번들 파일이 생성된다.
생성된 파일중 .manifest 는 필요한 경우 (버전 체크 등) 포함시키고
확장자가 없는 파일만 다운로드 서버로 업로드시키면 된다.
여기서 AssetBundles 는 없어도 되며
아마 스트리밍 형태로 처리할 때 사용하는 게 아닌가 싶다. (확실히 모르겠다.)
마지막으로 리스트 형태로 여러 개 처리가 가능하게 대충 구성해본 에셋 매니저 코드이다.
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
public struct AssetBundleData
{
public string Name;
public string Url;
public AssetBundleData(string name, string url = null)
{
Name = name;
Url = url;
}
}
public class AssetBundleManager : SingletonStatic<AssetBundleManager>
{
readonly string AssetBundleDirectory = Application.streamingAssetsPath + "/AssetBundles/";
Dictionary<string, AssetBundle> bundles = new Dictionary<string, AssetBundle>();
/// <summary>
/// AssetBundle 을 다운받아 로컬에 저장합니다.
/// </summary>
public void DownloadAssetBundle(string name, string url, Action<float> progressFunc = null, Action completeFunc = null)
{
StartCoroutine(DownloadAndCache(name, url, progressFunc, completeFunc));
}
/// <summary>
/// AssetBundle 을 순차적으로 다운받아 로컬에 저장합니다.
/// </summary>
public void DownloadAssetBundle(List<AssetBundleData> bundles, Action<float> progressFunc = null, Action<int> indexFunc = null, Action completeFunc = null)
{
if (bundles.Count >= 2)
{
void LoopFunc(int index)
{
indexFunc?.Invoke(index);
Debug.Log(index);
if (index == bundles.Count - 1)
DownloadAssetBundle(bundles[index].Name, bundles[index].Url, progressFunc, completeFunc);
else if (index < bundles.Count)
DownloadAssetBundle(bundles[index].Name, bundles[index].Url, progressFunc, () => LoopFunc(index + 1));
}
LoopFunc(0);
}
else if (bundles.Count > 0)
DownloadAssetBundle(bundles[0].Name, bundles[0].Url, progressFunc, completeFunc);
}
IEnumerator DownloadAndCache(string name, string url, Action<float> progressFunc, Action completeFunc)
{
using (var req = UnityWebRequest.Get(url))
{
var oper = req.SendWebRequest();
yield return DownloadProgress(oper, progressFunc);
if (req.result != UnityWebRequest.Result.Success)
Debug.LogError(req.error);
else
{
string dir = Path.GetDirectoryName(AssetBundleDirectory + name);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
File.WriteAllBytes(AssetBundleDirectory + name, req.downloadHandler.data);
}
}
completeFunc?.Invoke();
}
/// <summary>
/// AssetBundle 을 로컬에서 로드합니다.
/// </summary>
public void LoadAssetBundle(string name, Action<float> progressFunc = null, Action completeFunc = null)
{
if (!File.Exists(AssetBundleDirectory + name))
{
Debug.LogError($"AssetBundles : {name} 는 존재하지 않습니다.");
return;
}
if (bundles.ContainsKey(name))
{
Debug.LogError($"AssetBundles : {name} 는 이미 로드되었습니다.");
return;
}
StartCoroutine(LoadFromLocal(name, progressFunc, completeFunc));
}
/// <summary>
/// AssetBundle 을 순차적으로 로컬에서 로드합니다.
/// </summary>
public bool LoadAssetBundle(List<AssetBundleData> bundles, Action<float> progressFunc = null, Action<int> indexFunc = null, Action completeFunc = null)
{
bool error = false;
for (int i = 0; i < bundles.Count; i++)
{
if (!File.Exists(AssetBundleDirectory + bundles[i].Name))
{
Debug.LogError($"AssetBundles : [{i}]{bundles[i].Name} 는 존재하지 않습니다.");
error = true;
}
if (this.bundles.ContainsKey(bundles[i].Name))
{
Debug.LogError($"AssetBundles : [{i}]{bundles[i].Name} 는 이미 로드되었습니다.");
error = true;
}
}
if (error)
return false;
if (bundles.Count >= 2)
{
void LoopFunc(int index)
{
Debug.Log(index);
indexFunc?.Invoke(index);
if (index == bundles.Count - 1)
StartCoroutine(LoadFromLocal(bundles[index].Name, progressFunc, completeFunc));
else if (index < bundles.Count)
StartCoroutine(LoadFromLocal(bundles[index].Name, progressFunc, () => LoopFunc(index + 1)));
}
LoopFunc(0);
}
else if (bundles.Count > 0)
StartCoroutine(LoadFromLocal(bundles[0].Name, progressFunc, completeFunc));
return true;
}
IEnumerator LoadFromLocal(string name, Action<float> progressFunc, Action completeFunc)
{
var bundle = AssetBundle.LoadFromFileAsync(Path.Combine(AssetBundleDirectory, name));
yield return DownloadProgress(bundle, progressFunc);
bundles[name] = bundle.assetBundle;
completeFunc?.Invoke();
}
IEnumerator DownloadProgress(AsyncOperation operation, Action<float> progressFunc)
{
while (!operation.isDone)
{
progressFunc?.Invoke(operation.progress);
yield return null;
}
}
public T Load<T>(string bundleName, string path) where T : UnityEngine.Object
{
if (!bundles.ContainsKey(bundleName))
{
Debug.LogError($"AssetBundles : {bundleName} 는 없습니다.");
return default;
}
T value = bundles[bundleName].LoadAsset<T>(path);
if (value == null)
{
Debug.LogError($"AssetBundles : {path} 는 없습니다.");
return default;
}
return value;
}
}
bundles = new List<AssetBundleData>();
bundles.Add(new AssetBundleData("image/background", "경로"));
bundles.Add(new AssetBundleData("image/sprite", "경로"));
bundles.Add(new AssetBundleData("model", "경로"));
bool LocalLoad()
{
return AssetBundleManager.Instance.LoadAssetBundle(bundles,
progressFunc: ProgressUpdate,
indexFunc: (i) => currentIndex = i,
completeFunc: Complete);
}
if (!LocalLoad())
{
Debug.Log("로컬 실패. 서버에서 다운로드");
AssetBundleManager.Instance.DownloadAssetBundle(bundles,
progressFunc: ProgressUpdate,
indexFunc: (i) => currentIndex = i,
completeFunc: () => LocalLoad());
}
필자의 경우 다운로드 서버는 테스트기 때문에 대충 NAS 에 올려서 사용했다.
(다이렉트 링크 지원이 안돼서 적절하진 않다.)
위 코드를 활용한다고 하면 버전 체크 정도만 추가하면 되지 않을까 싶다.
추가로 혹시나 해서 적어두지만
게임 빌드 시에는 에셋 번들로 지정한 리소스나 빌드한 리소스는 자동으로 제외되기 때문에
빌드 전에 옮겨놓거나 할 필요는 없다.
다만, 테스트 도중 생긴 StreamingApplication.streamingAssetsPath 에 있는 건
같이 빌드가 되기 때문에 삭제해줘야 한다.
'게임 프로그래밍 > 유니티' 카테고리의 다른 글
시간 제어에 대한 간단한 테스트 (0) | 2021.01.01 |
---|