Unity 어드레서블 애셋

정선호·2023년 7월 1일
0

Unity Features

목록 보기
9/28

참고 자료

공식 문서

참고 영상

로컬 사용

강의 영상

패키지 설치 및 초기 세팅

설치 및 창 오픈

Package Manager에서 Addressables을 임포트한다.
메뉴바의 Window - Asset Management - Addressables - Group을 눌러 Addressables Groups창을 연다.

Addressables Groups 창의 Profile - Manage Profiles를 선택해 Addressables Profiles창을 연다.
해당 창에서는 서버에서 에셋을 다운로드 할 때 사용할 서버 주소를 입력할 수 있다.

초기 세팅

Addressables Groups의 기본 로컬 그룹(Defalult Local Group(Default)) 인스펙터 창에서 Cache Clear BehaviourClear When New Version Loaded 선택

  • 설정 시 새 버전의 번들을 다운로드 할 때 이전 버전 번들을 자동으로 삭제

이후 Inspect Top Level Settings를 클릭해 다음과 같이 세팅

  • Unique Bundle IDs 체크(기존에 변경점이 없는 번들들은 무시하게 됨)
  • Send Profiler Events 체크(이벤트 뷰어를 활성화해 상황 확인 가능)
  • Build Remote Catalog 체크(카탈로그 사본을 생성해 서버에 업로드)
    • Build & Load PathsRemote로 설정

다시 기본 로컬 그룹에 들어와 다음과 같이 세팅

  • Build & Load PathsRemote로 설정

프리팹 등록 및 분류

오브젝트 프리팹

프리팹과 해당 프리팹이 사용하는 머터리얼들의 Addressable을 체크하여 어드레서블 애셋으로 등록

그룹 및 라벨로 에셋 분류 가능

  • 라벨은 하나의 에셋에 여러 개를 중복해 등록할 수 있지만 하나의 라벨만 등록하는 것을 권장함
  • 새로 그룹을 만들 시 동일하게 초기 세팅을 해준다

오디오 및 이미지 프리팹

원하는 오디오 파일 혹은 이미지 파일의 Addressable을 체크하여 어드레서블 에셋으로 등록 가능

스크립트로 프리팹 사용

선언 및 참조

스크립트에 AssetReference 형식으로 프리팹들을 참조할 수 있다.

  • AssetRefernceGameObject : 게임 오브젝트 형식
  • AssetReferenceSprite : 2D Sprite
  • AssetReferenceT<> : 제네릭 형식

이외에 다양한 형식이 존재한다.

이후 인스펙터 창에서 원하는 프리팹을 목록 중 선택, 그룹창, 폴더에서 드래그 등으로 넣는다.

사용

public class AddressableMananger : MonoBehaviour
{
    [SerializeField] private AssetReferenceGameObject cubeObj;
    [SerializeField] private AssetReferenceGameObject[] buildingObjs;

    [SerializeField] private AssetReferenceT<AudioClip> soundBGM;
    [SerializeField] private AssetReferenceSprite flagSprite;

	// 추후 Release해줄 때 사용하기 위한 오브젝트 레퍼런스 리스트
    private List<GameObject> _objects = new List<GameObject>();

    [SerializeField] private AudioSource bgmObj;
    [SerializeField] private Image flagImage;

    // Start is called before the first frame update
    void Start()
    {
        StartCoroutine(InitAddressable());
    }

    private IEnumerator InitAddressable()
    {
        var init = Addressables.InitializeAsync();
        yield return init;
    }

    public void Button_SpawnObject()
    {
    	// 단일 게임오브젝트 생성
        cubeObj.InstantiateAsync().Completed += (obj) =>
        {
            _objects.Add(obj.Result);
        };

		// 다중 게임오브젝트 생성
        for (int i = 0; i < buildingObjs.Length; i++)
        {
            buildingObjs[i].InstantiateAsync().Completed += (obj) =>
            {
                _objects.Add(obj.Result);
            };
        }

        soundBGM.LoadAssetAsync().Completed += (clip) =>
        {
            bgmObj.clip = clip.Result;
            bgmObj.loop = true;
            bgmObj.Play();
        };

        flagSprite.LoadAssetAsync().Completed += (img) =>
        {
            flagImage.sprite = img.Result;
        };
    }
}

Build

Addressables Groups창에서 Build 0 New Build - Default Build Script 실행

빌드를 마친 후 Play Mode Script - Use Asset Database 선택

게임을 실행한 후 Button_SpawnObject() 함수 실행 시 미리 지정해둔 오브젝트를 실행할 수 있음

Release

위의 클래스에 Release함수를 추가해준다.

    public void Button_Release()
    {
        // LoadAssetAsync로 로드한 애셋을 Release해준다
        soundBGM.ReleaseAsset();
        flagSprite.ReleaseAsset();
        
        if (_objects.Count == 0)
            return;

        var index = _objects.Count - 1;

        for (int i = _objects.Count - 1; i > -1; i--)
        {
            // InstantiateAsync로 생성한 오브젝트들을 Release해준다
            Addressables.ReleaseInstance(_objects[i]);
            _objects.RemoveAt(i);
        }
    }

서버 사용

강의 영상

미리 로컬에서 위와 같이 에셋을 추가 및 설정한 후 서버를 적용할 수 있다

AWS S3 설정

1.AWS S3의 버킷을 만든다.

  • '모든 퍼블릭 액세스 차단' 체크 해제
  • '현재 설정으로 인해 이 버킷과 그 안에 포함된 객체가 퍼블릭 상태가 될 수 있음을 알고 있습니다.' 체크

2.버킷 페이지에 들어가 다음과 같이 설정해준다.

  • 권한 페이지에 진입
  • '버킷 정책 편집' 클릭
  • '버킷 ARN' 복사 후 '정책 생성기 버튼' 클릭
  • 'Select Type of Policy'를 'S3 Bucket Policy'로 선택
  • 'Principal'에는 *를 입력, 'Actions'에서는 'GetObject'선택
  • 'ARN'에는 아까 복사해둔 버킷 ARN 붙여넣기
  • 'Add Statement'버튼으로 규정 추가, 'Generate Policy'버튼 클릭
  • 팝업된 JSON구문을 복사해 정책 칸에 붙여넣기, "Resource"구문의 마지막에 /*추가
  • 변경 사항 저장 추가

3.아무 파일을 하나 업로드한 후 해당 파일의 객체 URL 주소를 복사한다.

Unity 설정

경로 설정

1.Remote Profile을 다음과 같이 설정한다.

  • 복사한 객체 URL주소를 Addressable ProfileRemote.LoadPath에 붙여넣은 후 파일 이름을 [BuildTarget]으로 변경해준다.

2.Addressable Group들의 Build & Load PathsRemote인지, Path Preview로 보이는 경로가 올바른지 확인한다. 시스템 세팅에서도 변경되었는지 확인한다.

3.AWS S3에 올린 파일을 삭제한다.

빌드 설정

1.Addressable GroupsBuild - Clear Build Cache - All을 선택해 이전 빌드 캐시 파일들을 삭제한다.

2.프로젝트의 파일 탐색기에서 Assets - ServerData내에 있는 Standalone...파일을 삭제한다.

3.Addressable GroupsBuild - New Build - Default Build Script로 재빌드

  1. 파일 탐색기의 Assets - ServerData내에 있는 Standalone...폴더를 AWS S3에 추가

  2. Addressable GroupsPlay Mode ScriptUse Existing Build로 선택해 서버에서 데이터를 받아오게 변경

서버에서 다운로드된 파일 위치

C:\Users\유저_이름\AppData\LocalLow\Unity\프로젝트_이름에 다운로드 된다.
테스트를 위해 다운로드 파일을 삭제하고 싶을 시 해당 폴더를 삭제하면 된다.

로딩 및 실행 스크립트

다운로드 매니저

어드레서블 에셋의 다운로드 필요 여부를 확인하고 만약 필요하면 다운로드를 실행하는 씬의 매니저

public class DownloadManager : MonoBehaviour
{
    [Header("UI")]
    public GameObject waitMessage;
    public GameObject downMessage;
    public Slider downSlider;
    public TMP_Text sizeText;
    public TMP_Text downValText;

    [Header("Label")] // 다운로드받을 애셋들의 라벨
    public AssetLabelReference defaultLabel;
    public AssetLabelReference matLabel;

    // 다운받을 애셋들의 파일 크기
    private long _patchSize;
    // 각각의 애셋들의 파일 크기
    private Dictionary<string, long> _patchMap = new Dictionary<string, long>();


    // Start is called before the first frame update
    void Start()
    {
        downMessage.SetActive(false);
        waitMessage.SetActive(true);
        
        StartCoroutine(InitAddressable());
        StartCoroutine(CheckUpdateFiles());
    }

    // 어드레서블을 초기화해주는 코루틴
    private IEnumerator InitAddressable()
    {
        var init = Addressables.InitializeAsync();
        yield return init;
    }

    // 업데이트할 파일을 체크하는 코루틴
    // 각 라벨별로 다운로드할 데이터를 파악한 후 추가 파일이 있으면 다운로드 버튼 활성화
    // 아니면 로딩 씬을 거쳐 샘플신을 로드하게끔 실행
    private IEnumerator CheckUpdateFiles()
    {
        var labels = new List<string>() { defaultLabel.labelString, matLabel.labelString };

        _patchSize = default;

        foreach (var label in labels)
        {
            var handle = Addressables.GetDownloadSizeAsync(label);
            yield return handle;
            _patchSize += handle.Result;

            if (_patchSize > Decimal.Zero)
            {
                waitMessage.SetActive(false);
                downMessage.SetActive(true);

                sizeText.text = "" + GetFileSize(_patchSize);
            }
            else
            {
                downValText.text = "100%";
                downSlider.value = 1f;
                yield return new WaitForSeconds(2f);
                LoadingManager.LoadScene("SampleScene");
            }
        }
    }

    public void Button_Download()
    {
        StartCoroutine(PatchFiles());
    }

    // 새 파일에 대한 패치를 실시하는 코루틴
    // 각 라벨에 대해 다운로드 여부를 파악한 후 다운로드할 파일이 있으면 다운로드 코루틴 시작
    // 직후 다운로드 상태 코루틴 시작
    private IEnumerator PatchFiles()
    {
        var labels = new List<string>() { defaultLabel.labelString, matLabel.labelString };

        _patchSize = default;

        foreach (var label in labels)
        {
            var handle = Addressables.GetDownloadSizeAsync(label);
            yield return handle;

            if (handle.Result != Decimal.Zero)
            {
                StartCoroutine(DownLoadFromLabel(label));
            }
        }

        yield return CheckDownloadStatus();
    }

    // 각각의 라벨에 대해 다운로드하게끔 하는 코루틴
    // 해당 라벨에 대해 다운로드 데이터를 받아온 후 핸들을 Release
    private IEnumerator DownLoadFromLabel(string label)
    {
        _patchMap.Add(label, 0);

        var handle = Addressables.DownloadDependenciesAsync(label);

        while (!handle.IsDone)
        {
            _patchMap[label] = handle.GetDownloadStatus().DownloadedBytes;
            yield return new WaitForEndOfFrame();
        }

        _patchMap[label] = handle.GetDownloadStatus().TotalBytes;
        Addressables.Release(handle);
    }

    // 다운로드 현황을 체크하는 코루틴
    // 로딩바와 진행도를 갱신해주고 완료시 로딩신 후 샘플신으로 넘겨주기
    private IEnumerator CheckDownloadStatus()
    {
        var total = 0f;
        downValText.text = "0 %";

        while (true)
        {
            total += _patchMap.Sum(tmp => tmp.Value);

            var perValue = total / _patchSize;
            downSlider.value = perValue;
            downValText.text = string.Format("{0:##.##}", perValue * 100) + " %";

            if (total.Equals(_patchSize))
            {
                LoadingManager.LoadScene("SampleScene");
                break;
            }

            total = 0f;
            yield return new WaitForEndOfFrame();
        }
    }

    // 파일 사이즈에 따라 용량에 대한 
    public string GetFileSize(long byteCnt)
    {
        string size = "0 Bytes";

        if (byteCnt >= 1073741824.0)
        {
            size = string.Format("{0:##.##}", byteCnt / 1073741824.0) + " GB";
        }
        else if (byteCnt >= 1048576.0)
        {
            size = string.Format("{0:##.##}", byteCnt / 1048576.0) + " MB";
        }
        else if (byteCnt >= 1024.0)
        {
            size = string.Format("{0:##.##}", byteCnt / 1024.0) + " KB";
        }
        else if (byteCnt > 0 && byteCnt < 1024.0)
        {
            size = string.Format("{0:##.##}", byteCnt) + " KB";
        }
        
        return size;
    }
}

로딩 매니저

한 씬에서 다른 씬으로 로딩할 때 중간에 거쳐가는 신 매니저

public class LoadingManager : MonoBehaviour
{
    // 로딩할 신의 이름
    public static string nextScene;

    public Slider loadingBar;
    
    // Start is called before the first frame update
    void Start()
    {
        StartCoroutine(StartLoading());
    }

    // 지정된 신으로 로딩을 시작하는 코루틴
    private IEnumerator StartLoading()
    {
        yield return null;

        AsyncOperation op = SceneManager.LoadSceneAsync(nextScene);
        op.allowSceneActivation = false;

        float timer = 0f;

        while (!op.isDone)
        {
            yield return null;

            timer += Time.deltaTime;

            if (op.progress < 0.9f)
            {
                loadingBar.value = Mathf.Lerp(loadingBar.value, op.progress, timer);
                if (loadingBar.value >= op.progress)
                {
                    timer = 0f;
                }
            }
            else
            {
                loadingBar.value = Mathf.Lerp(loadingBar.value, 1f, timer);
                
                if (loadingBar.value.Equals(1f))
                {
                    yield return new WaitForSeconds(2f);
                    op.allowSceneActivation = true;
                    yield break;
                }
            }
        }
    }

    // 한 씬에서 다른 씬으로 넘길 때 사용하는 씬 로딩 함수
    public static void LoadScene(string sceneName)
    {
        nextScene = sceneName;
        SceneManager.LoadScene("LoadingScene");
    }
}

애셋 변경 후 서버에 패치하기

  1. 에셋 파일 변경(파일의 이름은 그대로 하는 것을 권장) 후 Addressables Groups 에서Build - Update a Previous Build를 선택해 빌드

    • 이후에 파일 탐색기가 팝업되는데 이 때 '빌드하려는 목표 OS 폴더'안에 있는 .bin파일을 선택해 빌드
  2. 프로젝트 파일의 ServerData에 들어가서 파일의 수정 날짜 및 시간이 빌드한 날짜인 파일들을 선택.
    이들이 새로 변경된 파일들이다.

  3. AWS S3에 진입해 .json, .hash, 에셋을 변경한 로컬 그룹의 Bundle파일을 삭제한 후 변경된 파일을 업로드한다.


어드레서블 사용의 주의점

중복 애셋파일 방지 방법

Addressable 2.6.0 버전에 존재하는 Analyzer를 사용해 중복 애셋을 한 곳에 몰아넣는 방식을 사용한다.

이 때 중복 애셋을 몰아넣기 위해 자동으로 생성된 그룹은 옵션만 바꿔주고 안의 내용물을 추가하거나 삭제하면 안 된다. (연결되어 있는 모든 번들이 새로 빌드되어 데이터 낭비가 심해짐)

Prevent Updates 항목을 체크해두는 습관을 두자. 빌드를 시도할때마다 거슬리게 그룹이 하나씩 생성될거지만 모든 번들이 다시 빌드되는것보다는 나으니까...

씬에 중복 파일 배치하지 않기

어드레서블에 로드되는 프리팹을 씬에 배치하게 되면 씬 데이터와 애셋 번들에 중복되게 저장된다.

씬 데이터는 .Unity 형식으로 애플리케이션 데이터로 저장되고, 애셋 번들은 따로 추출되어 서버에 업로드되기 때문이다.

따라서 어드레서블에 추가한 프리팹은 씬에 배치하지 말고 스크립트로 가져와 생성해야 한다.

profile
학습한 내용을 빠르게 다시 찾기 위한 저장소

0개의 댓글