Addressables

이오싶·2025년 10월 15일

Unity의 Addressables(정식 명칭: Addressable Asset System) 은 리소스를 효율적으로 로딩·관리·배포할 수 있게 도와주는 Unity의 공식 에셋 관리 시스템이다.

핵심 개념

Address

  • 에셋마다 주소(Address) 라는 식별자가 부여된다.
    • 주소를 통해 코드에서 에셋을 직접 참조하지 않고 문자열이나 키 기반으로 에셋을 로드할 수 있다.

      Addressables.LoadAssetAsync<GameObject>("EnemyPrefab");
    • 에셋의 경로나 배치가 변경되더라도, 주소만 유지하면 코드 변경 없이 로드가 가능하다.

  • 어드레서블 시스템은 카탈로그에 어드레서블 주소와 실제 에셋 데이터의 저장 경로를 매핑하여 저장하며, 설정에 따라 어드레서블 주소를 통해 실제 에셋을 로컬 저장소나, 별도의 로컬 캐시나, 서버 등 원하는 위치에서 간편하게 가져올 수 있다.
    • Local Catalog
      • 플레이어 빌드 내에 저장
    • Remote Catalog
      • 리모트 서버 내에 업로드, 플레이어 빌드에 의해 런타임 다운로드
      • 빌드 퍼블리싱 없이 컨텐츠 업데이트 가능
      • 카탈로그 URL을 대응시켜야 함

Asset Group

  • 에셋은 그룹 단위로 묶여 관리된다.
  • 그룹은 빌드/배포 정책(로컬에 포함할지, 원격 서버에서 받을지, 압축 방식 등)을 지정할 수 있다.

그룹 생성 및 관리

Windows > Asset Management > Addressables > Groups

동적 비동기 로딩 / 메모리 관리

  • InstantiateAsync, LoadAssetAsync 등을 통해 에셋을 비동기 로딩
  • 사용이 끝난 에셋은 Addressables.Release로 해제하여 메모리 누수 방지
    var handle = Addressables.InstantiateAsync("EnemyPrefab");
    // ...
    Addressables.Release(handle);

기존 플젝에서 Resources.Load을 사용하던 로직에서 Addressables로 변경하는 경우 동기→비동기 로직 변경으로 골머리를 좀 앓을 수도 있음. UniTask를 적극 활용하는 것을 추천.

예제: Citrus Heaven

https://github.com/Iosif2510/CitrusHeaven

<Doodle God>이나 <Infinite Craft>같은 원소 조합하기 계열의 단순한 드래그/드롭 게임을 뚝딱뚝딱 만들어보았음

각 Citrus 데이터는 ScriptableObject Asset 형태로 저장되며, 해금된 Citrus 목록 및 조합식에서는 각 Citrus의 이름만을 저장

이때 CitrusData는 어드레서블로 지정되어 Assets/GameData/Citrus/{이름}.asset 주소를 부여

public static async UniTask<CitrusData> FindCitrusByNameAsync(string name)
{
    var handle = Addressables.LoadAssetAsync<CitrusData>($"Assets/GameData/Citrus/{name}.asset");
    await handle.ToUniTask();
    if (handle.Status == AsyncOperationStatus.Succeeded)
    {
        var result = handle.Result;
        handle.Release();
        return result;
    }

    Debug.LogError($"Failed to load CitrusData for {name}. Status: {handle.Status}");
    return null;
}

FindCitrusByNameAsync를 사용해 필요한 경우에 이름으로 주소를 재구성해 비동기적으로 ScriptableObject 에셋을 로드하는 것이 가능

public static async UniTask<Dictionary<(CitrusData, CitrusData), CitrusData>> DeserializeCraftTableAsync(string json)
{
    var table = JsonUtility.FromJson<CraftTableList>(json);
    var craftTable = new Dictionary<(CitrusData, CitrusData), CitrusData>();

    var tasks = new List<UniTask<(CitrusData, CitrusData, CitrusData)>>();
    foreach (var entry in table.entries)
    {
        tasks.Add(UniTask.Create(async () =>
        {
            var taskResults = await UniTask.WhenAll(
                FindCitrusByNameAsync(entry.operand1),
                FindCitrusByNameAsync(entry.operand2),
                FindCitrusByNameAsync(entry.result)
            );
            return (taskResults.Item1, taskResults.Item2, taskResults.Item3);
        }));
    }

    var results = await UniTask.WhenAll(tasks);
    foreach (var result in results)
    {
        if (result.Item1 != null && result.Item2 != null && result.Item3 != null)
        {
            craftTable.Add((result.Item1, result.Item2), result.Item3);
        }
    }
    return craftTable;
}

Citrus의 이름만 담겨있는 CraftTable.jsonDictionary<(CitrusData, CitrusData), CitrusData> 형태로 역직렬화하는 등으로 활용.

다만 json 파일 내에서 동일한 Citrus들이 여러번 중복되어 등장하는데, 이미 로드된 CitrusData를 캐싱하고 활용하는 것이 아니므로 위 코드는 최적화의 여지가 좀 더 있을듯…

profile
Iosif2510

0개의 댓글