Notice
Recent Posts
Recent Comments
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Today
Total
Archives
관리 메뉴

cyphen156

유니티 핵심기능 - Addressable 본문

프로젝트/유니티

유니티 핵심기능 - Addressable

cyphen156 2025. 4. 14. 19:39

유니티의 빌드파일 경량화 및 로딩 최적화를 위한 2번째 솔루션 어드레서블이다.

다음 문서들을 참고하여 작성하였다.

https://docs.unity3d.com/kr/current/Manual/com.unity.addressables.html

 

어드레서블 - Unity 매뉴얼

 

docs.unity3d.com

https://docs.unity3d.com/kr/Packages/com.unity.addressables@1.21/manual/index.html

 

Addressables 패키지 | Addressables | 1.21.17

Addressables 패키지 Addressables 패키지는 애플리케이션의 콘텐츠를 구성하고 패키징하는 툴과 스크립트, 런타임에 에셋을 로드 및 해제하는 API를 제공합니다. 에셋을 어드레서블로 만들면 해당 에

docs.unity3d.com

우선 어드레서블이라는 기능을 사용하려면 Unity Registry에서 제공하는 Addressables라는 패키지를 다운로드 받아야 한다.

기본 설정은 다음과 같다.

모든 설정을 완료하고 나면 스트리밍의 형태로 애셋을 사용할 수 있게 된다.

프로젝트를 어드레서블로 전환하기

1. 씬 데이터 전환

우선 다음과 같이 씬 리스트에서 기존씬들을 모두 제거하고 초기화 씬만 등록해 놓는다.

그 다음 기존 씬을 선택해보면 인스펙터 창에 어드레서블이라는 체크박스가 생긴것을 확인할 수 있다. 이것을 활성화 해주면 씬파일이 어드레서블화 된다.

주의해야 할 점은 initScene은 절대로 어드레서블화 해선 안된다. 애플리케이션 진입점이 사라지기 때문이다.

이제 씬들이 스트리밍 데이터 형식으로 로드되기 때문에 더 이상 프로젝트에서 SceneManager를 사용할 수 없다. 대신 Addressable 클래스에 존재하는 SceneLoaing 메서드를 사용해야 한다.

그래서 씬 전체를 어드레서블화 할지 또는 내부 에셋 로드만 어드레서블로 관리할지는 선택사항이다. 앱 자체에서 기본적으로 씬을 들고 추가로 무거운 에셋들만 로드할지(플레이어 기준 일부분만 먼저 앱로드하고 어드레서블로 나머지 공간들을 로드할지)

그래서 나는 타이틀 씬만 진입씬으로 활용하고 나머지는 어드레서블로 관리할 예정이다.

프리펩 또는 폴더 자체를 어드레서블화 해서 관리할 수 있다.

그 다음 중요한 것이 그룹화이다.

실제로 어드레서블 그룹은 빌드 할 때 그룹별로 빌드되기 때문에 어떤 그룹을 언제 가져와서 로드할지 결정하는 가장 중요한 요소이다.

어드레서블 자체가 애셋 번들로 구성되어 있기 때문에 그룹단위로 빌드되어 있다. 

그래서 얼마나 세세하게 쪼개서 빌드하느냐가 중요해진다.

빨간색은 처음 빌드할때 쓴다.

두번째는 한번 빌드한 프로필을 다시 빌드할 때 쓴다.

세번째는 뭔가 문제가 생겼을 때 쓰는 빌드다. 기존 빌드 캐시를 모두 지우고 클린 빌드하는 방법이다.

완료 되면 다음과 같이 결과 창이 나온다.

데이터가 저장된 위치는 프로파일에서 설정된 위치로 간다. 

다음과 같은 경고문이 있으니 왠만하면 기본설정에서 바꾸지 말도록 하자

더보기

[!경고] 로컬 빌드 또는 로드 경로를 기본값에서 바꾸지 마십시오. 바꾼 경우 플레이어 빌드를 생성하기 전에 로컬 빌드 아티팩트를 커스텀 빌드 위치에서 프로젝트의 StreamingAssets 폴더로 복사해야 합니다. 또한 이러한 경로를 변경하면 플레이어 빌드의 일부로 어드레서블을 빌드할 수 없습니다.

그리고 로컬 환경이 아닌 외부에서 업로드 하여 데이터를 다운로드 받아 사용할 경우 다음과 같이 안내하고 있다.

더보기

RemoteBuildPath 에 빌드하는 그룹이 있는 경우 해당 에셋 번들, 카탈로그, 해시 파일을 호스팅 서버에 업로드해야 합니다. 프로젝트에서 원격 콘텐츠를 사용하지 않는 경우 모든 그룹이 로컬 빌드 및 로드 경로를 사용하도록 설정하십시오.

콘텐츠 빌드는 어드레서블이 플레이어 빌드에서 직접 사용하지 않는 다음 파일도 생성합니다.

  • addressables_content_state.bin: 콘텐츠 업데이트 빌드를 만드는 데 사용됩니다. 동적 콘텐츠 업데이트를 지원하는 경우 각 콘텐츠 릴리스 후에 이 파일을 저장해야 합니다. 그렇지 않은 경우에는 이 파일을 무시할 수 있습니다.
  • AddressablesBuildTEP.json: 빌드 성능 데이터를 로깅합니다. 자세한 내용은 빌드 프로파일링을 참조하십시오.

콘텐츠 빌드를 설정 및 수행하는 방법에 대한 자세한 내용은 어드레서블 콘텐츠 빌드를 참조하십시오.

이 안에 빌드된 번들이 다음과 같이 존재한다.

이제 이 빌드된 어드레서블 번들을 로컬 로드 해보겠다.

어드레서블 애셋 로드

어드레서블 애셋의 로드 방법은 세가지가 있다. 

  • AssetReference : 인스펙터 창에서 관리 할 수 있는 일종의 자동화 스크립트 기반 로드 방법 리소스 로드와 비슷한 방식이다.
  • 주소별 로드 : 문자열을 통해 어드레서블 애셋을 개별 로드하는 방식
  • 레이블 별 로드 : 주소별 로드와 비슷하지만 빌드 했을 때 설정된 레이블을 키로 사용하여 한번에 로드하는 방식

나는 프로젝트가 씬단위로 빌드되니까 레이블 별 로드와 주소별 로드를 섞어서 사용하도록 해야 할 것이다.

원격 빌드의 경우 다음과 같이 체크버튼을 활성화 하고 로드패스와 빌드 패스를 선택하여 설정해주어야 한다.

리소스 로딩하기

이제 남은것은 리소스를 로딩하는 것이다. 

리소스 로드의 경우 ResoruceManager를 싱글턴으로 구성하여 동기식 로드를 사용하였다. 

보통 게임 플레이 중 다운로드를 진행하기 위하여 어드레서블 다운로드 / 메모리 로드의 경우 비동기로 작업을 하지만 나의 경우는 TitleScene을 제외한 모든 씬을 어드레서블 리소스로 사용하기 때문에 초기에 메모리에 적재되어있지 않다. 

그렇기 때문에 다운로드 / 로드 작업을 동기식으로 진행하고, UI에서 화면을 가려주는 Loading UI를 Coroutine으로 비동기 처리하여 모든 필수 데이터의 다운로드 / 메모리 로드가 완료 된 후 씬을 변경할 수 있도록 보장하였다.

조금더 확장한다면 비동기 / 동기 다운로드를 Bool 플레그를 사용하여 특정 코드의 실행 여부만을 점검한다면 하나의 함수로 두 개의 기능을 동작하게 할 수 있을것 같다. 

외부 호출용 LoadScene()

호출되면 애셋을 서버로부터 다운로드 받고, 다운로드가 완료 되면 로드 된 씬을 비동기로 오픈한다.

public void LoadScene(string sceneName)
{
    // 씬 관련 애셋 프리로드
    if (!LoadAssetsWithLabelSync(sceneName))
    {
        Debug.LogError($"[GameManager] 리소스 동기 로딩 실패: {sceneName}");
        return;
    }

    // 씬 로드
    var sceneHandle = Addressables.LoadSceneAsync(sceneName + "Scene", LoadSceneMode.Single);
    sceneHandle.Completed += (op) =>
    {
        if (op.Status == AsyncOperationStatus.Succeeded)
        {
            Debug.Log($"[GameManager] 씬 {sceneName} 비동기 로드 완료");
        }
        else
        {
            Debug.LogError($"[GameManager] 씬 {sceneName} 비동기 로딩 실패");
        }
    };

LoadAssetsWithLabelSync()

씬이 로드되기 전 애셋 번들을 로컬 저장소에 라벨(그룹)별로 동기적으로 다운로드 받고, 메모리에 적재하는 함수

// 그룹 동기 로딩 /// 씬과 연관된 애셋 로드
public bool LoadAssetsWithLabelSync(string label)
{
    if (!keys.Contains(label))
    {
        keys.Add(label);
    }

    UIManager.instance.StartLoad();

    // 번들 동기 다운로드
    var downloadHandle = Addressables.DownloadDependenciesAsync(label);
    downloadHandle.WaitForCompletion();


    // 번들 동기 로드
    labelHandle = Addressables.LoadAssetsAsync<GameObject>(
        label,
        obj =>
        {
            if (obj != null && !loadedAssets.ContainsKey(obj.name))
                loadedAssets[obj.name] = obj;
        });

    labelHandle.WaitForCompletion();

    UIManager.instance.EndLoad();
    return labelHandle.Status == AsyncOperationStatus.Succeeded;
}

 

downloadHandle.WaitForCompletion();

비동기 작업이 완료 되었을 때 다음 코드를 실행하는 동기화 락 코드

ReleaseResources()

더 이상 에셋 번들 그룹이 필요하지 않아 졌을 때 메모리를 절약하기 위해 개별 해제하는 함수

// 그룹 해제
private void ReleaseResources()
{
    if (labelHandle.IsValid())
    {
        Addressables.Release(labelHandle);
        labelHandle = default;
    }
}

OnDestroy()

싱글턴 리소스 매니저가 파괴될 때 적재된 메모리를 모두 해제하는 함수

private void OnDestroy()
{
    ReleaseResource();
    ReleaseResources();
}

전체 코드는 다음과 같습니다.

ResourceManager.cs

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.SceneManagement;

public class ResourceManager : MonoBehaviour
{
    public static ResourceManager instance;

    public string key = string.Empty;
    public List<string> keys = new List<string>();

    private AsyncOperationHandle<GameObject> handle;
    private AsyncOperationHandle<IList<GameObject>> labelHandle;

    private Dictionary<string, GameObject> loadedAssets = new Dictionary<string, GameObject>();
    
    private void Awake()
    {
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else 
        {
            Destroy(gameObject);
        }

    }

    // 개별 로딩 /// 씬 로드시 사용 - 그룹 로딩 연결

    public void LoadScene(string sceneName)
    {
        // 씬 관련 애셋 프리로드
        if (!LoadAssetsWithLabelSync(sceneName))
        {
            Debug.LogError($"[GameManager] 리소스 동기 로딩 실패: {sceneName}");
            return;
        }

        // 씬 로드
        var sceneHandle = Addressables.LoadSceneAsync(sceneName + "Scene", LoadSceneMode.Single);
        sceneHandle.Completed += (op) =>
        {
            if (op.Status == AsyncOperationStatus.Succeeded)
            {
                Debug.Log($"[GameManager] 씬 {sceneName} 비동기 로드 완료");
            }
            else
            {
                Debug.LogError($"[GameManager] 씬 {sceneName} 비동기 로딩 실패");
            }
        };
    }

    // 개별 에셋 비동기 로드
    public void LoadAsset(string key)
    {
        handle = Addressables.LoadAssetAsync<GameObject>(key);
        handle.Completed += Handle_Completed;
    }

    private void Handle_Completed(AsyncOperationHandle<GameObject> operation)
    {
        if (operation.Status == AsyncOperationStatus.Succeeded)
        {
            Instantiate(operation.Result, transform);
        }
        else
        {
            Debug.LogError($"Asset for {key} failed to load.");
        }
    }

    // 개별 해제
    private void ReleaseResource()
    {
        if (handle.IsValid())
        {
            Addressables.Release(handle);
            handle = default;
        }
    }

    // 그룹 동기 로딩 /// 씬과 연관된 애셋 로드
    public bool LoadAssetsWithLabelSync(string label)
    {
        if (!keys.Contains(label))
        {
            keys.Add(label);
        }

        UIManager.instance.StartLoad();

        // 번들 동기 다운로드
        var downloadHandle = Addressables.DownloadDependenciesAsync(label);
        downloadHandle.WaitForCompletion();


        // 번들 동기 로드
        labelHandle = Addressables.LoadAssetsAsync<GameObject>(
            label,
            obj =>
            {
                if (obj != null && !loadedAssets.ContainsKey(obj.name))
                    loadedAssets[obj.name] = obj;
            });

        labelHandle.WaitForCompletion();

        UIManager.instance.EndLoad();
        return labelHandle.Status == AsyncOperationStatus.Succeeded;
    }
    private void LoadHandle_Completed(AsyncOperationHandle<IList<GameObject>> operation)
    {
        if (operation.Status != AsyncOperationStatus.Succeeded)
        {
            Debug.LogWarning("Some assets did not load.");
        }

        UIManager.instance.EndLoad();
    }

    // 그룹 해제
    private void ReleaseResources()
    {
        if (labelHandle.IsValid())
        {
            Addressables.Release(labelHandle);
            labelHandle = default;
        }
    }

    public GameObject GetAsset(string name)
    {
        if (loadedAssets.TryGetValue(name, out var obj))
        {
            return obj;
        }
        Debug.LogWarning($"[ResourceManager] GetAsset 실패: {name}가 로딩되지 않음");
        return null;
    }

    private void OnDestroy()
    {
        ReleaseResource();
        ReleaseResources();
    }
}