Deff_Dev

[Unity/C#] 어드레서블 에셋 로드 본문

Unity(유니티)/유니티 공부

[Unity/C#] 어드레서블 에셋 로드

Deff_a 2024. 7. 11. 00:26

 

어드레서블에 저장된 리소스를 Load 할 때, 필요할 때마다 Load 할 것인지, 미리 모든 데이터를 Load 해서 저장해놓을 것인지에 대해 내가 구현한 방법을 설명하겠다.

 

일단 리소스의 용도를 먼저 생각해서, 

게임에서 계속 사용해야되는 리소스는 게임이 시작될 때, 전부 불러와서 메모리에 저장해두고 사용하는 방식으로 구현했다.

잠깐만 사용하는 리소스는 리소스의 사용이 끝나면, 언로드하는 방식으로 구현했다.


구현

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;

public class ResourceManager : Singleton<ResourceManager>
{
    [field: SerializeField] public List<PoolObject> UnitDataList { get; private set; }
    [field: SerializeField] public List<PoolObject> EnemyDataList { get; private set; }
    private Dictionary<int, GameObject> uiDictionary = new Dictionary<int, GameObject>();

    public async Task<List<PoolObject>> LoadPoolObjectData(EAddressableType type ) // 로드 에셋
    {
        List<PoolObject> poolObjList = new List<PoolObject>();
        // Load
        for (int i = 0; i < AddressableManager.Instance.LocationDict[(int)type].Count; i++)
        {
            var obj = await AddressableManager.Instance.LoadAsset<PoolObject>(type, i);
            if (obj != null)
            {
                poolObjList.Add(obj);
            }
            else
            {
                Debug.LogError($"{i} number of Unit Load Error");
            }
        }

        return poolObjList;
    }

    public async Task<GameObject> GetUIGameObject(EUIRCode uiType) // 로드 UI
    {
        GameObject obj;
        int idx = (int)uiType;
        if (uiDictionary.ContainsKey(idx))
        {
            obj = uiDictionary[idx];
        }
        else
        {
            obj = await AddressableManager.Instance.InstantiateAsync(EAddressableType.UI, idx);
            uiDictionary.Add(idx, obj);
        }
        obj.SetActive(true);
        return obj;
    }
    
    [ContextMenu("rel Enemy")]
    public void ReleaseElementalEnmey()
    {
        AddressableManager.Instance.ReleaseInstance(EnemyDataList);
    }

    [ContextMenu("load Enemy")]
    public async void LoadEnemyAsset()
    {
        EnemyDataList = await LoadPoolObjectData(EAddressableType.FireEnemy);
    }
    public async void LoadUnitAsset()
    {
        UnitDataList = await LoadPoolObjectData(EAddressableType.Unit);
    }
    
}

 

Unit은 계속 사용하기 때문에 전부 불러온 뒤, UnitDataList 에 저장해둔다.

 

UI 같은 경우에는 처음 한 번만 어드레서블에서 불러온 다음, 딕셔너리에 저장해두고 다음에 활성화 시킬 땐 딕셔너리에 있는 오브젝트를 활성화한다.

 

Enemy 같은 경우에는 맵 컨셉마다 등장하는 Enemy가 정해져 있기 때문에 컨셉에 맞는 Enemy만 불러온 뒤, EnemyDataList에 저장해둔다.

맵이 바뀐다면 기존에 불러온 Enemy들을 언로드해주고 해당 맵에 맞는 Enemy를 불러오는 방식으로 구현했다.

 

어드레서블 코드.cs

더보기
using System;
using System.Linq;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceLocations;

public enum EAddressableType
{
    Unit,
    UI,
    FireEnemy,
}

public enum EFireEnemyRCode
{
    FE001,
    FE002,
    FE003,
    FE004,
    FE005,
}
public enum EUnitRCode
{
    FC001,
    FR001,
    FU001, 
    FL001,
}

public class AddressableManager : Singleton<AddressableManager>
{
    // 어드레서블의 Label을 얻어올 수 있는 필드.
    public AssetLabelReference[] assetLabels;
    
    // IResourceLocation : 리소스의 경로를 저장한다
    public Dictionary<int, IList<IResourceLocation>> LocationDict { get;  private set; } = new Dictionary<int, IList<IResourceLocation>>();
    
    private void Awake()
    {
        GetLocations();
    }

    public async void GetLocations()
    {
        // 딕셔너리에 리소스 경로 할당
        await LoadLocationsAsync();
        
        SortLocationDictionary();
        
        ResourceManager.Instance.LoadUnitAsset(); // 유닛 로드
    }

    private async Task LoadLocationsAsync()
    {
        // 빌드타겟의 경로를 가져온다.
        for (int i = 0; i < assetLabels.Length; i++)
        {
            var handle = Addressables.LoadResourceLocationsAsync(assetLabels[i].labelString);

            // 비동기 작업 완료를 기다림
            await handle.Task;

            // 결과를 할당
            LocationDict[i] = handle.Result;
        }
    }

    private Type GetAddressableCode(EAddressableType type)
    {
        Type code = type switch
        {
            EAddressableType.Unit => typeof(EUnitRCode),
            EAddressableType.UI => typeof(EUIRCode),
            EAddressableType.FireEnemy => typeof(EFireEnemyRCode),
            _ => throw new NotImplementedException()
        };
        
        return code;
    }
    private void SortLocationDictionary() // 로케이션 정렬
    {
        // flag L 검색 조건
        BindingFlags flag = BindingFlags.Instance | BindingFlags.NonPublic;
        for (int i = 0; i < LocationDict.Count; i++)
        {
            Type enumType = GetAddressableCode((EAddressableType)i);
            MethodInfo method = typeof(AddressableManager).GetMethod(nameof(SortLocations),flag)?.MakeGenericMethod(enumType);
            LocationDict[i] = (IList<IResourceLocation>)method?.Invoke(this, new object[] { LocationDict[i]});
        }
        
        LocationDict[(int)EAddressableType.Unit] = SortLocations<EUnitRCode>(LocationDict[(int)EAddressableType.Unit]);
        LocationDict[(int)EAddressableType.UI] = SortLocations<EUIRCode>(LocationDict[(int)EAddressableType.UI]);
        LocationDict[(int)EAddressableType.FireEnemy] = SortLocations<EFireEnemyRCode>(LocationDict[(int)EAddressableType.FireEnemy]);
    }

    private IList<IResourceLocation> SortLocations <T> (IList<IResourceLocation> locations ) where T : Enum // 어드레서블 자동 정렬
    {
        IList<IResourceLocation> temp = new List<IResourceLocation>();
        
        foreach (T enumValue in Enum.GetValues(typeof(T)))
        {
            string enumName = enumValue.ToString();
            IResourceLocation location = locations.FirstOrDefault(loc =>  loc.PrimaryKey.Contains(enumName));
            if (location != null)
            {
                temp.Add(location);
            }
        }
        return temp;
    }

    public async Task<GameObject> InstantiateAsync(EAddressableType type ,int idx)
    {
        // 해당 라벨의 idx번째 리소스를 생성한다.
        var handle = Addressables.InstantiateAsync(LocationDict[(int)type][idx]);
        await handle.Task;

        if (handle.Status == AsyncOperationStatus.Succeeded) // 성공
        {
            GameObject obj = handle.Result.gameObject;
            return obj; // 반환
        }
        else // 실패
        {
            Debug.LogError("Failed to instantiate the object.");
            return default;
        }
    }
    
    // MonoBehaviour를 상속받는 데이터 형태만 T에 넣을 수 있도록 강제함.
    public async Task<T> LoadAsset <T> (EAddressableType type ,int idx) where T : MonoBehaviour
    {
        // 해당 라벨의 idx번째 리소스의 게임 오브젝트 정보를 저장 (LoadAsset은 생성 X, 정보만 저장)
        var handle = Addressables.LoadAssetAsync<GameObject>(LocationDict[(int)type][idx]);
        await handle.Task;

        if (handle.Status == AsyncOperationStatus.Succeeded) // 성공
        {
            GameObject loadedGameObject = handle.Result;
            // 게임 오브젝트 정보를 MonoBehaviour를 상속받는 T의 정보를 저장
            T component = loadedGameObject.GetComponent<T>(); 
            if (component != null)
            {
                return component;
            }
            else
            {
                Debug.LogError($"The loaded GameObject does not have a component of {typeof(T)}.");
                return default;
            }
        }
        else
        {
            Debug.LogError("Failed to LoadAsset the object.");
            return default;
        }
    }
    
    public void ReleaseInstance <T> (List<T> assetList) where T : MonoBehaviour// 생성된 오브젝트 제거
    {
        for (int i = assetList.Count - 1; i >= 0; i--)
        {
            Addressables.ReleaseInstance(assetList[i].gameObject);
            assetList.Remove(assetList[i]);
        }
    }
}