AssetBundle 가이드
앱인토스 Unity 게임에서 AssetBundle을 효율적으로 사용하여 앱 크기를 최소화하고 동적 콘텐츠 로딩을 구현하는 방법을 제공해요.
1. 앱인토스 AssetBundle 전략
AssetBundle 구성 전략
📦 앱인토스 AssetBundle 구조
├── Core Bundle (필수 번들) - 10MB 이내
│ ├── 게임 엔진 핵심 에셋
│ ├── 앱인토스 SDK 리소스
│ ├── 기본 UI 시스템
│ └── 토스 브랜딩 에셋
├── Feature Bundles (기능별 번들) - 각 5MB 이내
│ ├── 게임플레이 번들
│ ├── 사운드 번들
│ ├── 이펙트 번들
│ └── 토스페이 연동 번들
├── Level Bundles (레벨별 번들) - 각 3MB 이내
│ ├── 스테이지 1-10
│ ├── 스테이지 11-20
│ └── 보스 레벨
└── Seasonal Bundles (시즌 컨텐츠) - 각 2MB 이내
├── 이벤트 에셋
├── 한정 스킨
└── 특별 아이템AssetBundle 관리자 구현
c#
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.IO;
public class AppsInTossAssetBundleManager : MonoBehaviour
{
public static AppsInTossAssetBundleManager Instance { get; private set; }
[System.Serializable]
public class BundleConfig
{
public string bundleName;
public BundlePriority priority;
public string cdnUrl;
public long bundleSize;
public string version;
public bool persistent = true;
public List<string> dependencies = new List<string>();
}
public enum BundlePriority
{
Critical = 0, // 앱 시작 전 필수 로딩
High = 1, // 게임 시작 시 로딩
Medium = 2, // 백그라운드 로딩
Low = 3, // 필요시 로딩
Toss = 4 // 앱인토스 특화 번들
}
[Header("번들 설정")]
public BundleConfig[] bundleConfigs;
[Header("앱인토스 CDN 설정")]
public string tossCdnBaseUrl = "https://cdn.appintoss.com/bundles/";
public bool enableTossCaching = true;
public long maxCacheSizeMB = 100;
// 내부 관리
private Dictionary<string, AssetBundle> loadedBundles = new Dictionary<string, AssetBundle>();
private Dictionary<string, BundleConfig> bundleConfigMap = new Dictionary<string, BundleConfig>();
private HashSet<string> downloadingBundles = new HashSet<string>();
void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
InitializeBundleManager();
}
else
{
Destroy(gameObject);
}
}
void InitializeBundleManager()
{
// 번들 설정 맵 생성
foreach (var config in bundleConfigs)
{
bundleConfigMap[config.bundleName] = config;
}
// 로컬 캐시 정리
StartCoroutine(CleanupOldBundles());
// Critical 우선순위 번들 자동 로딩
StartCoroutine(LoadCriticalBundles());
}
IEnumerator LoadCriticalBundles()
{
var criticalBundles = System.Array.FindAll(bundleConfigs,
b => b.priority == BundlePriority.Critical || b.priority == BundlePriority.Toss);
foreach (var bundle in criticalBundles)
{
yield return StartCoroutine(LoadBundleCoroutine(bundle.bundleName));
}
Debug.Log("Critical AssetBundle 로딩 완료");
}
public void LoadBundleAsync(string bundleName, System.Action<bool> onComplete = null)
{
StartCoroutine(LoadBundleAsyncCoroutine(bundleName, onComplete));
}
IEnumerator LoadBundleAsyncCoroutine(string bundleName, System.Action<bool> onComplete)
{
yield return StartCoroutine(LoadBundleCoroutine(bundleName));
onComplete?.Invoke(IsBundleLoaded(bundleName));
}
IEnumerator LoadBundleCoroutine(string bundleName)
{
if (IsBundleLoaded(bundleName))
{
yield break;
}
if (downloadingBundles.Contains(bundleName))
{
yield return new WaitUntil(() => !downloadingBundles.Contains(bundleName));
yield break;
}
downloadingBundles.Add(bundleName);
var config = bundleConfigMap[bundleName];
// 의존성 번들 먼저 로딩
foreach (var dependency in config.dependencies)
{
if (!IsBundleLoaded(dependency))
{
yield return StartCoroutine(LoadBundleCoroutine(dependency));
}
}
// 번들 다운로드 또는 로컬 로딩
string bundlePath = GetBundlePath(bundleName);
AssetBundle bundle = null;
if (File.Exists(bundlePath))
{
// 로컬에서 로딩
var bundleLoadRequest = AssetBundle.LoadFromFileAsync(bundlePath);
yield return bundleLoadRequest;
bundle = bundleLoadRequest.assetBundle;
}
else
{
// CDN에서 다운로드
yield return StartCoroutine(DownloadBundle(config));
if (File.Exists(bundlePath))
{
var bundleLoadRequest = AssetBundle.LoadFromFileAsync(bundlePath);
yield return bundleLoadRequest;
bundle = bundleLoadRequest.assetBundle;
}
}
if (bundle != null)
{
loadedBundles[bundleName] = bundle;
Debug.Log($"AssetBundle 로딩 성공: {bundleName}");
// 앱인토스 분석에 번들 로딩 성공 전송
SendBundleAnalytics(bundleName, true, 0);
}
else
{
Debug.LogError($"AssetBundle 로딩 실패: {bundleName}");
SendBundleAnalytics(bundleName, false, 0);
}
downloadingBundles.Remove(bundleName);
}
IEnumerator DownloadBundle(BundleConfig config)
{
string downloadUrl = tossCdnBaseUrl + config.bundleName;
string savePath = GetBundlePath(config.bundleName);
Debug.Log($"AssetBundle 다운로드 시작: {config.bundleName}");
float startTime = Time.realtimeSinceStartup;
using (var www = new WWW(downloadUrl))
{
yield return www;
if (string.IsNullOrEmpty(www.error))
{
// 파일 저장
Directory.CreateDirectory(Path.GetDirectoryName(savePath));
File.WriteAllBytes(savePath, www.bytes);
float downloadTime = Time.realtimeSinceStartup - startTime;
Debug.Log($"AssetBundle 다운로드 완료: {config.bundleName} ({downloadTime:F2}초)");
SendBundleAnalytics(config.bundleName, true, downloadTime);
}
else
{
Debug.LogError($"AssetBundle 다운로드 실패: {config.bundleName} - {www.error}");
SendBundleAnalytics(config.bundleName, false, 0);
}
}
}
string GetBundlePath(string bundleName)
{
return Path.Combine(Application.persistentDataPath, "Bundles", bundleName);
}
// 에셋 로딩 API
public void LoadAssetFromBundle<T>(string bundleName, string assetName,
System.Action<T> onComplete, System.Action<string> onError = null) where T : UnityEngine.Object
{
StartCoroutine(LoadAssetFromBundleCoroutine<T>(bundleName, assetName, onComplete, onError));
}
IEnumerator LoadAssetFromBundleCoroutine<T>(string bundleName, string assetName,
System.Action<T> onComplete, System.Action<string> onError) where T : UnityEngine.Object
{
// 번들이 로딩되어 있지 않으면 먼저 로딩
if (!IsBundleLoaded(bundleName))
{
yield return StartCoroutine(LoadBundleCoroutine(bundleName));
}
if (!IsBundleLoaded(bundleName))
{
onError?.Invoke($"번들 로딩 실패: {bundleName}");
yield break;
}
var bundle = loadedBundles[bundleName];
var assetRequest = bundle.LoadAssetAsync<T>(assetName);
yield return assetRequest;
if (assetRequest.asset != null)
{
onComplete?.Invoke(assetRequest.asset as T);
}
else
{
onError?.Invoke($"에셋을 찾을 수 없습니다: {assetName}");
}
}
// 토스 특화 번들 로딩
public void LoadTossBundles(System.Action onComplete = null)
{
StartCoroutine(LoadTossBundlesCoroutine(onComplete));
}
IEnumerator LoadTossBundlesCoroutine(System.Action onComplete)
{
var tossBundles = System.Array.FindAll(bundleConfigs,
b => b.priority == BundlePriority.Toss);
foreach (var bundle in tossBundles)
{
yield return StartCoroutine(LoadBundleCoroutine(bundle.bundleName));
}
Debug.Log("토스 특화 번들 로딩 완료");
onComplete?.Invoke();
}
// 번들 언로딩
public void UnloadBundle(string bundleName, bool unloadAllLoadedObjects = false)
{
if (loadedBundles.ContainsKey(bundleName))
{
loadedBundles[bundleName].Unload(unloadAllLoadedObjects);
loadedBundles.Remove(bundleName);
Debug.Log($"AssetBundle 언로드: {bundleName}");
}
}
// 캐시 관리
IEnumerator CleanupOldBundles()
{
string bundleDir = Path.Combine(Application.persistentDataPath, "Bundles");
if (!Directory.Exists(bundleDir))
{
yield break;
}
var files = Directory.GetFiles(bundleDir);
long totalSize = 0;
foreach (var file in files)
{
totalSize += new FileInfo(file).Length;
}
long maxCacheSize = maxCacheSizeMB * 1024 * 1024;
if (totalSize > maxCacheSize)
{
Debug.Log($"번들 캐시 정리 시작: {totalSize / (1024 * 1024)}MB > {maxCacheSizeMB}MB");
// 최근 접근 시간 기준으로 정렬
var fileInfos = new List<FileInfo>();
foreach (var file in files)
{
fileInfos.Add(new FileInfo(file));
}
fileInfos.Sort((a, b) => a.LastAccessTime.CompareTo(b.LastAccessTime));
// 오래된 파일부터 삭제
foreach (var fileInfo in fileInfos)
{
if (totalSize <= maxCacheSize) break;
totalSize -= fileInfo.Length;
File.Delete(fileInfo.FullName);
Debug.Log($"오래된 번들 삭제: {fileInfo.Name}");
}
}
}
void SendBundleAnalytics(string bundleName, bool success, float downloadTime)
{
var analyticsData = new Dictionary<string, object>
{
{"bundle_name", bundleName},
{"success", success},
{"download_time", downloadTime},
{"timestamp", System.DateTime.UtcNow.ToString("o")}
};
AppsInToss.SendAnalytics("assetbundle_loading", analyticsData);
}
// 공개 API
public bool IsBundleLoaded(string bundleName)
{
return loadedBundles.ContainsKey(bundleName);
}
public string[] GetLoadedBundleNames()
{
var names = new string[loadedBundles.Count];
loadedBundles.Keys.CopyTo(names, 0);
return names;
}
public long GetTotalCacheSize()
{
string bundleDir = Path.Combine(Application.persistentDataPath, "Bundles");
if (!Directory.Exists(bundleDir))
{
return 0;
}
long totalSize = 0;
var files = Directory.GetFiles(bundleDir);
foreach (var file in files)
{
totalSize += new FileInfo(file).Length;
}
return totalSize;
}
}2. 번들 빌드 최적화
자동화된 번들 빌드 시스템
c#
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
using System.IO;
using System.Collections.Generic;
public class AppsInTossBundleBuilder
{
[MenuItem("AppsInToss/Build All Bundles")]
public static void BuildAllBundles()
{
BuildBundlesForTarget(BuildTarget.Android);
BuildBundlesForTarget(BuildTarget.iOS);
Debug.Log("앱인토스 번들 빌드 완료");
}
static void BuildBundlesForTarget(BuildTarget target)
{
string outputPath = Path.Combine("AssetBundles", target.ToString());
if (!Directory.Exists(outputPath))
{
Directory.CreateDirectory(outputPath);
}
// 앱인토스 최적화 설정
var buildOptions = BuildAssetBundleOptions.ChunkBasedCompression |
BuildAssetBundleOptions.StrictMode |
BuildAssetBundleOptions.DeterministicAssetBundle;
// 번들 빌드 실행
var manifest = BuildPipeline.BuildAssetBundles(
outputPath,
buildOptions,
target
);
if (manifest != null)
{
OptimizeBundlesForAppsInToss(outputPath, manifest);
GenerateBundleConfig(outputPath, manifest);
}
}
static void OptimizeBundlesForAppsInToss(string outputPath, AssetBundleManifest manifest)
{
var allBundles = manifest.GetAllAssetBundles();
foreach (var bundleName in allBundles)
{
string bundlePath = Path.Combine(outputPath, bundleName);
var fileInfo = new FileInfo(bundlePath);
// 번들 크기 체크 (앱인토스 권장 크기)
long sizeMB = fileInfo.Length / (1024 * 1024);
if (sizeMB > 10) // 10MB 초과
{
Debug.LogWarning($"번들 크기가 권장 크기를 초과합니다: {bundleName} ({sizeMB}MB)");
}
// 압축률 정보 출력
Debug.Log($"번들 정보: {bundleName} - {sizeMB}MB");
}
}
static void GenerateBundleConfig(string outputPath, AssetBundleManifest manifest)
{
var config = new AppsInTossBundleConfig();
var allBundles = manifest.GetAllAssetBundles();
foreach (var bundleName in allBundles)
{
string bundlePath = Path.Combine(outputPath, bundleName);
var fileInfo = new FileInfo(bundlePath);
var bundleInfo = new AppsInTossBundleConfig.BundleInfo
{
name = bundleName,
size = fileInfo.Length,
hash = manifest.GetAssetBundleHash(bundleName).ToString(),
dependencies = manifest.GetAllDependencies(bundleName)
};
config.bundles.Add(bundleInfo);
}
// JSON으로 설정 파일 저장
string configPath = Path.Combine(outputPath, "bundle_config.json");
string json = JsonUtility.ToJson(config, true);
File.WriteAllText(configPath, json);
Debug.Log($"번들 설정 파일 생성: {configPath}");
}
}
[System.Serializable]
public class AppsInTossBundleConfig
{
[System.Serializable]
public class BundleInfo
{
public string name;
public long size;
public string hash;
public string[] dependencies;
}
public List<BundleInfo> bundles = new List<BundleInfo>();
public string version = "1.0.0";
public string buildTime;
public AppsInTossBundleConfig()
{
buildTime = System.DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss");
}
}
#endifAssetBundle을 사용해 앱 크기를 줄이고, 필요한 콘텐츠만 동적으로 업데이트하세요. 앱인토스 환경에서는 번들 크기와 다운로드 속도에 특히 주의해야 해요.
