게임 리소스 관리

게임 클라이언트 개발(이하 게임 개발)이 다른 클라이언트 개발과 가장 큰 차이점을 보이는 것이 리소스입니다. 일반적으로 게임에서는 더 다양하고 많은 양의 이미지, 사운드, 밸런스 데이터 등의 다양한 리소스들을 사용합니다. 최근의 게임 개발은 개발의 효율성을 위해 게임 엔진을 사용하는 것이 당연시 되었고, 엔진들은 리소스들을 어느정도 관리해주지만 그 수준이 리소스의 쉬운 세팅과 디렉토리 관리 정도일 뿐입니다.

여러 리소스들을 다뤄야하는 게임에서, 개발을 쉽고 빠르게 개발할 수 있게 도와주는 도구인 엔진을 맹신하여 의존성을 마구 만들어내는 코드를 작성하는 모습을 자주 볼 수 있습니다. 대부분이 엔진에서 제공하는 리소스 로더들을 직접 참조하고 리소스가 존재하는 디렉토리를 하드코딩하여 읽어오는 방식입니다.

개발이 진행되면서 프로그램이 계속 변화하는 것 처럼 리소스 또한 변화합니다. 디자이너와 협업을 할 경우 효율적인 리소스 관리를 위해 디렉토리 구조를 변경할 수도 있고, 파일의 이름을 변경할 수도 있습니다. 만약 리소스가 하드코딩이 되어있는 상황이라면 각각의 클래스를 확인하면서 이전 디렉토리를 새로운 디렉토리로 바꿔주는 작업을 해야할 것입니다.

또한 하드코딩된 리소스를 포함하는 클래스들이 리소스가 변경되거나 새롭게 추가되었을 때, 이러한 리소스가 제대로 적용되었는지 테스트 하는 방법은 직접 게임을 돌려보는 방법 뿐이게 됩니다. 이런 방식은 사람이 일일이 화면을 보며 확인을 해야하고, 리소스에 대한 테스트를 위해 리소스 로딩 뿐만 아니라 테스트에 영향을 줄 수 있는 다른 기능들도 수행이 되어야합니다. 때문에 테스트에 적합하지 않아 변화에 취약하고 버그를 발생시키기 쉽습니다.

확장성을 높히는 설정파일

리소스를 로드하기 위해 오브젝트들이 알아야하는 정보는 리소스가 어디에 위치하는지 그리고 그 리소스의 이름이 무엇인지에 대한 정보입니다. 하드코딩되어 각 클래스에 분산되어 존재하는 리소스 파일에 대한 정보를 한번에 모아서 관리할 수 있다면 일일이 리소스가 로드되는 시점을 찾으며 이를 수정하는 작업을 최소화할 수 있습니다.

메타 데이터 파일

유니티 엔진을 예로 들면, 스크립트, 이미지, 사운드 또는 유니티에서 만든 게임 오브젝트등 디렉토리로 관리하는 루트 폴더인 Assets 하위의 폴더와 파일들은 .meta 확장자를 갖는 파일을 유니티가 생성해줍니다. 그리고 이 메타파일 안에는 해당 파일이 다른 파일과 갖는 연결정보 등을 포함합니다.

일반적인 상용 게임에서도 각 게임 컨텐츠에 대한 정보를 메타 데이터로 관리 될 수 있도록 엑셀 등의 파일로 따로 관리합니다. 이런 메타 데이터들은 각 게임 컨텐츠와 게임 오브젝트에 대한 정보를 담고 있기 때문에 이런 정보를 사용하고 있다면, 이 메타 데이터에 리소스롸 관련된 컬럼을 추가해서 리소스 정보를 담도록 할 수 있습니다.

하지만 이런 메타 데이터를 사용하지 않는 경우엔 이 방법을 사용할 수 없을 뿐더러 게임 데이터에 대한 정보를 담고 있는 메타 데이터에 관련 없는 리소스에 대한 정보를 추가하는 것은 그다지 추천하고 싶지 않습니다. 그리고 각 게임 오브젝트에 대해 리소스를 할당하므로 같은 리소스를 사용하는 다른 게임 오브젝트의 경우 같은 리소스 정보가 데이터 안에 중복해서 나타나는 문제점이 있습니다.

정보를 포함하는 다른 파일을 만드는 방법

텍스트 기반의 파일을 만들고 그 안에 리소스 파일들의 정보를 작성하여 사용하는 방법입니다다. 예시는 xml 파일로 구성되었지만 json이나 yaml 같은 다른 파일 형식을 갖더라도 상관이 없습니다. 여기에 추가적으로 수행되어야 할 작업은 이렇게 구성된 파일을 로드하고 파싱하는 클래스를 작성하는 것입니다.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="character0">Resources/Character/character000</string>
    <string name="character1">Resources/Character/character001</string>
    ...
</resources>

보통 위와같은 형식을 갖는 파일들은 키-밸류의 형식으로 맵핑이 되기 때문에 같은 리소스 정보가 중복되어 나타나진 않지만, 키로 사용하는 값을 게임 오브젝트에서 알고 있어야 한다는 한계가 존재합니다.

클래스로 만드는 방법

클래스를 사용하여 리소스에 대한 정보를 저장하는 방식입니다.

public static class ResourceConfig
{
    public const string PLAYER_IMG_DIR = "Resources/Player/";
    
    public static string GetPlayerImage(string characterName, int level)
    {
        var imgDir = PLATER_IMG_DIR + GetPlayerImageName(characterName, level);
        return imgDir;
    }
    
    private static string GetPlayerImageName(string characterName, int level)
    {
        ... // Predefined name regular expressions
        return imgName;
    }
    ...
}

다른 방법들과 다른 가장 큰 특징은 리소스 정보에 대한 정규표현을 사용할 수 있다는 것입니다. 루트 디렉토리의 정보는 const 변수로 설정하고 중간 디렉토리와 파일 이름에 대한 정보는 미리 아티스트와 협의된 정규표현을 통해 구현합니다. 때문에 존재하지 않는 리소스를 참조하기 위한 시도를 할 수도 있습니다. 만약 아티스트가 실수로 파일을 업로드하지 않았거나, 이름을 잘못 설정한 경우 이미지가 제대로 로드되지 않습니다. 하지만 이는 테스트를 통해 간단히 해결될 수 있습니다.

리소스 로딩과 관리

많은 수의 리소스가 존재하는 게임에서 한번에 메모리에 로드가 된다면 더이상 할당할 메모리 공간이 부족한 상황이 발생할 수 있기 때문에 게임에서 리소스들은 메모리에 실시간으로 로드되고 해제됩니다. 하지만 런타임에 실시간으로 로드되고 해제되는 리소스들이 각 게임 오브젝트에서 개별적으로 로드하고 있는 상황에선 관리하기 힘듬니다.

각각의 게임 오브젝트에서 개별적인 로드를 통해 리소스가 로딩될 경우 리소스가 언제 메모리에 올라오는지가 파편화되어 예상하기 힘들기 때문입니다. 때문에 리소스가 로드될 때 해당 게임 오브젝트 뿐만 아니라 제3의 오브젝트가 로딩하는 과정에 참여해 로딩된 리소스들이 관리되게 하여 파편화 현상을 줄일 수 있습니다.

리소스 로더 랩핑

엔진에서 제공하는 리소스 로더를 감싸는 커스텀 리소스 로더를 만들어 게임 오브젝트에 제공하는 방법입니다. 해당 커스텀 로더를 사용하는 클라이언트(게임 오브젝트)의 입장에선같은 일을 수행해주는 그저 다른 이름의 오브젝트일 수 있지만, 그 내부에서 어떤 리소스가 호출 되는지에 대한 정보를 관리할 수 있습니다.

public static class CustomResourceLoader
{
    public static T Load<T>(string path) where T : UnityEngine.Object
    {
        // 리소스 관리에 필요한 작업
        return Resources.Load<T>(path);
    }
}

하지만 이 방법은 단순한 구조의 게임이라면 상관없겠지만, 게임에서 필요로 하는 게임 오브젝트나 리소스의 종류가 많을 경우에 적합하지 않을 수 있습니다. 팩토리 오브젝트에서 게임 오브젝트를 생성할 때 사용되는 리소스의 종류가 다양하다는 말은 그 만큼 다양한 조건이 있다는 뜻입니다. 리소스의 관리는 그런 조건들에 따라 달라져야 하는데 이 경우엔 커스텀 리소스 로더 한곳에 모든 조건이 모이게 되어 복잡한 코드가 될 수 있습니다.

리소스 인젝션

각 게임 오브젝트의 리소스 로드에 대한 모든 권한을 다른 오브젝트에 위임하는 방식입니다.

public class GameObjectFactory : MonoBehaviour
{
    [SerializeField] private GameObjectController gameObjectPrefab;
    
    public void CreateGameObject()
    {
        var controller = Instantiate(gameObjectPrefab);
        // 게임 오브젝트에 대한 리소스 세팅
    }
}

게임 오브젝트에서 사용되는 리소스들은 해당 게임 오브젝트와 함께 로드되기 때문에 게임 오브젝트를 생성하는 팩토리 오브젝트에게 리소스 로드의 권한을 함께 주면 됩니다. 이렇게 구현을 하게 되면 게임 오브젝트에선 리소스 로딩에 대한 부분에는 신경 쓰지 않고 해당 게임 오브젝트의 로직에만 집중할 수 있게된다.