게임 리소스 관리

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

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

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

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

확장성을 높히는 설정파일

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

메타 데이터 파일

유니티 엔진을 예로 들면, 스크립트, 이미지, 사운드 또는 유니티에서 만든 게임 오브젝트등 디렉토리로 관리하는 루트 폴더인 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);
        // 게임 오브젝트에 대한 리소스 세팅
    }
}

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