Deff_Dev

[Unity/C#] 어드레서블 그룹 순서 이슈 해결 본문

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

[Unity/C#] 어드레서블 그룹 순서 이슈 해결

Deff_a 2024. 7. 10. 23:59

 

어드레서블 그룹 설정을 할 때, 리소스 순서가 내 마음대로 설정되지 않는 이슈가 있었다.

 

[Unity/C#] 어드레서블 (Addressable) 사용

어드레서블에 대해 공부를 하고 현재 진행 중인 프로젝트에 적용시켰다.구현 unit 리소스들은 게임 시작시 데이터를 받아오고, ui들은 필요할 때마다 어드레서블에서 해당 리소스를 동적 생성하

deff-dev.tistory.com


해결

 

리소스의 이름과 Enum 요소의 이름을 똑같이 설정하고 경로를 저장하는 IResourceLocation 리스트를 Enum이름에 맞게 순서를 정렬을 해주는 방식으로 위 문제를 해결했다.


IResourceLocation 정렬 

Enum 형태를 T로 받고 해당 Enum 요소 값을 탐색하면서 리소스 이름과 Enum 요소 값이 같다면 IResourceLocation 리스트에 Add 하고 탐색이 다 끝난 후에 Return 한다.

    private IList<IResourceLocation> SortLocations <T> (IList<IResourceLocation> locations ) where T : Enum // 어드레서블 자동 정렬
    {
    	// IList는 인스턴스화를 할 수 없기 때문에 List를 이용하여 인스턴스화 한다.
        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;
    }

 

FirstOrDefault

  • LINQ (Language Integrated Query) 라이브러리에서 지원하는 함수로, 조건에 맞는 요소들 중 첫 번째 요소를 반환
  • 만약 조건에 맞는 요소가 없다면 기본값(default value)을 반환

PrimaryKey

  • IResourceLocation에 저장된 리소스의 경로를 나타내는 키워드

정렬 함수 호출

 Type을 이용하여 정렬이 필요한 Enum 타입을 가져오고 MethodInfo를 통해 받아온 Type이용하여 정렬함수의 제네릭메소드를 만든 뒤, 해당 메소드를 불러온다.

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 : 검색 조건
        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]});
        }
     }

 

이 코드는 리플렉션을 이용한 방법이므로 상당히 비효율적인 방법이지만, 처음 실행될 때만 실행되는 코드이기도 하고 리플렉션을 사용해보고 싶어서 작성하게 된 코드이다.

 

    private void SortLocationDictionary() // 로케이션 정렬
    {
        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]);
    }

 

실제로는 이렇게 작성해도 동일하게 동작하지만, 어드레서블 유닛 타입이 추가될 때마다 한 줄씩 추가해야 하므로 하드 코딩의 느낌이 강하기 때문에, 추가적인 유지 보수가 필요 없는 리플렉션을 이용한 코드를 작성하게 됐다.

(실제로는 하드 코딩된 코드가 더 효율적이긴 하다.)

 

Enum 형을 반환하는 방법을 찾지 못해 Type으로 반환한 뒤, 다시 해당 TypeEnum으로 변환하는 방법을 사용했다.

 

// flag : 검색 조건
BindingFlags flag = BindingFlags.Instance | BindingFlags.NonPublic;

 

 

BindingFlags는 리플렉션이 해당 이름을 가진 메소드를 찾을 때 사용하는 검색 조건이다.

  • BindingFlags.Public: 공용(public) 멤버를 검색
  • BindingFlags.NonPublic: 비공용(non-public) 멤버(예: private, protected)를 검색
  • BindingFlags.Static: 정적(static) 멤버를 검색
  • BindingFlags.Instance: 인스턴스 멤버를 검색
Type enumType = GetAddressableCode((EAddressableType)i);

EAddressableType에 대한 Enum 타입을 얻는다.

MethodInfo method = typeof(AddressableManager).GetMethod(nameof(SortLocations),flag)?.
					MakeGenericMethod(enumType);

methodInfo를 이용하여 AddressableManagerflag에 일치하고 이름이 같은 메소드를 가져오는데, 해당 메소드는 제네릭 메소드 이기 때문에 enumType제네릭 타입 인수 <T>로 넣어준다.

 

LocationDict[i] = (IList<IResourceLocation>)method?.Invoke(this, new object[] { LocationDict[i]});

얻어온 method를 실행시키는데, 첫 번째 파라미터는 호출할 인스턴스를 넣어주고 두 번째 파라미터에 매개변수를 넣어준다. (앞에 데이터 형식은 반환 타입)


글을 마치며

IResourceLocation 리스트를 받아온 다음에 다시 한번 더 정렬을 한다는 게 마음에 걸리긴 하지만, 이 방법 말고는 문제를 해결할 다른 방법을 찾을 수 없었다.

 

궁극적으로 해결하기 위해서는 Tool을 만들어야 될 거 같다.

 

하지만 Tool을 만들기엔 할 작업이 많기 때문에 이 정도로 어드레서블 구조를 끝낼려고 한다.

 

추후에 시간이 남는다면 Tool 까지도 만들어보겠다.

 

그리고 리플렉션도 이번에 처음 써봤는데 써볼수록 느끼는 점은 흑마법 같다 생각이 든다.

 

너무 편리할 때가 있지만, 객체지향을 뒤집어 버린다는 느낌이 너무 강해서, 앞으로는 최대한 사용하는 것을 지양할 예정이다.


전체 코드.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]);
        }
    }
}