개인 프로젝트/프로젝트1

프레임워크 - 리소스 매니저

서보훈 2025. 2. 3. 16:14

Resources 폴더에서 프리팹이나 SO 등의 데이터를 가져오는 역할을 하는 매니저 입니다.

 

먼저 enum 형태로 리소스 폴더의 대분류, 소분류를 만들어주었습니다.

소분류의 경우 사용하지 않는 경우도 있기때문에 None 을 만들어주었습니다.

//파일 경로 (대분류)
public enum EMajorType
{
    Data,
    Prefab,
    Sound
}

//파일 경로 (세부)
public enum ESubType
{
    None,
    UI
}

일단 임시로 만들어준 상태입니다. 리소스 폴더의 내용이 늘어날때, enum 의 종류도 늘려주면 됩니다.

 

싱글톤의 경우, 별도로 만들어주어야합니다.

더보기

ResourceManager

//리소스 파일에서 데이터를 가져오는 클래스
public class ResourceManager : PersistentSingleton<ResourceManager>
{
    private Dictionary<string, object> resourcePool = new Dictionary<string, object>();

    /// <summary>
    /// 동기로 리소스 불러오기, key : 불러올 에셋 이름, majorType : 대분류, subType : 소분류 비어있을경우 None 으로 처리
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="key"></param>
    /// <param name="type"></param>
    /// <param name="categoryType"></param>
    /// <returns></returns>
    public T LoadResource<T> (string key, EMajorType majorType, ESubType subType = ESubType.None)
    {
        //리소스 폴더 경로 저장
        StringBuilder stringBuilder = new StringBuilder();

        //반환할 객체용
        T returnAsset;

        //경로 생성
        stringBuilder.Append(majorType);
        stringBuilder.Append((subType == ESubType.None) ? "" : $"/{subType}");
        stringBuilder.Append($"/{key}");

        //딕셔너리에서 같은 에셋이 있는지 검색
        if(!resourcePool.ContainsKey(stringBuilder.ToString()))
        {
            var targetResource = Resources.Load(stringBuilder.ToString(), typeof(T));

            //대상이 없을경우 처리
            if(targetResource == null )
            {
                return default;
            }

            resourcePool.Add(stringBuilder.ToString(), targetResource);
        }

        //반환 에셋 지정
        returnAsset = (T)resourcePool[stringBuilder.ToString()];

        return returnAsset;
    }

    /// <summary>
    /// 비동기로 리소스 불러오기, key : 불러올 에셋 이름, majorType : 대분류, subType : 소분류 비어있을경우 None 으로 처리
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="key"></param>
    /// <param name="majorType"></param>
    /// <param name="subType"></param>
    /// <returns></returns>
    public async Task<T> LoadResourceAsync<T>(string key, EMajorType majorType, ESubType subType = ESubType.None)
    {
        StringBuilder stringBuilder = new StringBuilder();

        T returnAsset;

        stringBuilder.Append(majorType);
        stringBuilder.Append((subType == ESubType.None) ? "" : $"/{subType}");
        stringBuilder.Append($"/{key}");

        if (!resourcePool.ContainsKey(stringBuilder.ToString()))
        {
            //비동기로 불러오기
            var op = Resources.LoadAsync(stringBuilder.ToString(), typeof(T));

            //처리가 완료될때까지 대기
            while(!op.isDone)
            {
                await Task.Yield();
            }

            var targetResource = op.asset;

            if(targetResource == null )
            {
                return default;
            }

            resourcePool.Add(stringBuilder.ToString(), targetResource);
        }

        returnAsset = (T)resourcePool[stringBuilder.ToString()];

        return returnAsset;
    }
}

 

동기와 비동기로 리소스를 가져오는 메서드를 나누어주었습니다.

동기의 경우, 씬을 로드할 때 사용하여 필요한 내용을 미리 로드하기 위해 사용합니다.

 

비동기의 경우 게임 진행중에 로드가 필요할경우 사용하게 될 것 입니다.

 

또한 이미 로드한 리소스를 다시 로드하지 않기 위해, 딕셔너리로 캐싱하는 방식을 사용하였습니다.

private Dictionary<string, object> resourcePool = new Dictionary<string, object>();

물론, 캐싱한 데이터가 많아질경우의 문제도 존재하기 때문에 적절한 위치에서 데이터를 비워줄 필요가 있을것입니다.

해당 문제는 차후에 생각해볼 예정입니다.

 

동기 형태로 로드하는 메서드 입니다.

public T LoadResource<T> (string key, EMajorType majorType, ESubType subType = ESubType.None)
{
    //리소스 폴더 경로 저장
    StringBuilder stringBuilder = new StringBuilder();

    //반환할 객체용
    T returnAsset;

    //경로 생성
    stringBuilder.Append(majorType);
    stringBuilder.Append((subType == ESubType.None) ? "" : $"/{subType}");
    stringBuilder.Append($"/{key}");

    //딕셔너리에서 같은 에셋이 있는지 검색
    if(!resourcePool.ContainsKey(stringBuilder.ToString()))
    {
        var targetResource = Resources.Load(stringBuilder.ToString(), typeof(T));

        //대상이 없을경우 처리
        if(targetResource == null )
        {
            return default;
        }

        resourcePool.Add(stringBuilder.ToString(), targetResource);
    }

    //반환 에셋 지정
    returnAsset = (T)resourcePool[stringBuilder.ToString()];

    return returnAsset;
}

리소스 폴더의 경로는 string 값으로 지정되며, 이 값을 만들어주기 위해 StringBuilder 를 사용해주었습니다.

딕셔너리에 같은 리소스가 있는지 경로 이름으로 key를 검색하며, 없을경우 경로를 key로 사용하여 리소스를 저장합니다.

 

딕셔너리에 저장이 완료되거나, 경로가 딕셔너리에 있을경우 저장된 리소스를 반환합니다.

 

비동기 방식의 경우, LoadAsync 를 사용하는 내용이 추가되었을 뿐, 크게 다른 내용은없습니다.

public async Task<T> LoadResourceAsync<T>(string key, EMajorType majorType, ESubType subType = ESubType.None)
{
    StringBuilder stringBuilder = new StringBuilder();

    T returnAsset;

    stringBuilder.Append(majorType);
    stringBuilder.Append((subType == ESubType.None) ? "" : $"/{subType}");
    stringBuilder.Append($"/{key}");

    if (!resourcePool.ContainsKey(stringBuilder.ToString()))
    {
        //비동기로 불러오기
        var op = Resources.LoadAsync(stringBuilder.ToString(), typeof(T));

        //처리가 완료될때까지 대기
        while(!op.isDone)
        {
            await Task.Yield();
        }

        var targetResource = op.asset;

        if(targetResource == null )
        {
            return default;
        }

        resourcePool.Add(stringBuilder.ToString(), targetResource);
    }

    returnAsset = (T)resourcePool[stringBuilder.ToString()];

    return returnAsset;
}

 

일단 기반 내용으로 이렇게 잡아주었습니다.

현재 완전한 기획이 잡힌 내용이 아니기 때문에, 이후 필요한 내용이 추가될 예정입니다.