SFML 과정중에 잠시 Framework를 언급 했었다.
Timber 게임을 만들어보면서 키 입력 같이 정의해놓고 어떤 프로젝트이던 쓸 법한 기능들이 많이 보였다. 그런 필요한 함수들을 구현해서 Framework를 업데이트 하고, SFML Template에 저장해두자.
Precompiled Header 즉 pch.h 은 컴파일 시간을 단축하기 위해, 많은 헤더파일들을 모아서 관리하기 위한 파일이다.
여러 소스에서 사용되는 iostream 같은 표준 헤더 파일, 혹은 사용자가 만든 헤더파일등을 관리하는 통합 헤더라고 보면 된다.
여러 소스파일마다 include 를 여러번 반복하는게 힘들어서 pch를 통해 정리하자.
// C++
#include <iostream>
#include <list>
#include <unordered_map>
// SFML
#include <SFML/Graphics.hpp>
#include "Defines.h"
#include "InputMgr.h"
#include "ResourceMgr.h"
#include "singleton.h"
리소스를 관리하는 리소스 매니저에는 두가지 방법이 있는데,
텍스처 폰트 사운드 버퍼를 통틀어 관리시키는 방법이 있고, 텍스처 폰트 사운드 매니저를
하나씩 만드는 방법이 있다.
리소스 매니저는 3가지 역할을 가지고 있는데, 리소스를 매개변수로 받으면 그 리소스를 실제로 로딩해주는 로드, 반대로 등록되어 있는 리소스를 지우고 메모리를 해제하는 언로드, 마지막으로 로드되어 있는 리소스를 탐색해서 리턴해주는 겟 까지 3가지가 있다.
텍스처 폰트 사운드 매니저를 각각 하나씩 만드려면 템플릿을 통해 리소스 매니저의 틀을 만들도록 하자.
template<typename T>
class ResourceMgr
{
private:
std::unordered_map<std::string, T*> resources;
public:
ResourceMgr() = default;
virtual ~ResourceMgr()
{
UnloadAll();
}
bool Load(const std::string& filePath);
void UnloadAll();
bool Unload(const std::string& filePath);
T* Get(const std::string& filePath);
ResourceMgr(const ResourceMgr&) = delete;
ResourceMgr(ResourceMgr&&) = delete;
ResourceMgr& operator=(const ResourceMgr&) = delete;
ResourceMgr& operator=(ResourceMgr&&) = delete;
};
std::unordered_map<std::string, T*> resources; 를 통해
키 값이 string 이고, T에 텍스처, 폰트등의 자료형이 들어가는 unordered_map 자료구조를 가진 변수 resources를 만든다.
복사 생성자, 복사대입 연산자는 오류가 생기지 않게 없애 두었고, 생성자와 소멸자는 기본이지만 소멸자에 등록된 리소스를 전부 해제하는 unloadAll 함수가 실행되게 했다.
이러한 관리자 객체들은 데이터를 입력받아 모아서 관리하는것을 맡기 때문에 역할에 맞는 효율적인 자료구조를 선택해야 한다. 보통은 탐색을 얼마나 많이 하냐에 따라 선택지가 다르다.
배열, 리스트와는 다른 자료 구조인 unordered_map과 map 이 있다.
기본적으로는 자료를 저장할때 고유한 식별자인 키(key) 와 실제 자료의 값을 쌍으로 가진
키-값 쌍을 가지고 있으며, 중복된 키 값을 허용하지 않는다. 두 자료구조의 다른 점이라면
map은 데이터의 순서를 중요시해 데이터를 키 값의 순으로 정렬하고, 탐색 시 해당 키의 값이 지금 탐색한것보다 크냐 작냐를 따지는 이진 탐색 트리 방식을 사용한다.
unordered_map은 자료의 순서를 따지지 않고, 해시 테이블을 사용하여 키 값을 저장한다.
해시 테이블이란, 해시 함수, 즉 자료 길이를 따지지 않는 임의의 값을 고유한 길이의 키 값으로 변환시키는 함수를 사용해 키 값을 저장한 것을 말한다.
리소스 매니저는 자료의 순서가 상관 없으니 탐색 시간이 빠른 unordered_map을 사용한다.
텍스처를 등록하는 load 함수를 만들어보자.
bool Load(const std::string& filePath)
{
if(resources.find(filePath) != resources.end())
{
return false;
}
T* res = new T();
bool success = res->loadFromFile(filePath);
if (success)
{ //resources[id] = res;
resources.insert({ filePath , res });
//resources.insert(std::unordered_map<std::string, T*>::make_pair(id,res))
}
return success;
}
중복된 키 값을 허용하지 않는 자료구조이기 때문에 성공 실패를 리턴하는 bool 형으로 만들었다.
파일 주소를 키 값으로 해서 받는다, 그 후
if(resources.find(filePath) != resources.end())
{
return false;
}
중복된 키 값이 있으면 resources.find(filePath) 가 1이고, resources.end()가 0이라 if문이 true가 되어서 실행되기 때문에 로드 실패를 리턴한다.
T* res = new T();
해당 데이터형을 가진 변수 res를 동적 할당한다.
bool success = res->loadFromFile(filePath);
이후 텍스처를 로드하는 loadFromFile 메소드를 사용하고, 만일 텍스처가 존재해서 성공적으로 로드했다면 1을, 실패했다면 0을 반환해 success에 저장한다.
이는 loadFromFile메소드 자체가 bool형을 반환하기 때문에 가능하다.
if (success)
{ //resources[id] = res;
resources.insert({ filePath , res });
//resources.insert(std::unordered_map<std::string, T*>::make_pair(id,res))
}
주석 처리한 부분도 다 비슷한 용례이지만, 간단하게 단일 키 값 쌍을 입력해주는 메소드 insert를 사용해 resources에 저장해준다. 이후 성공여부를 반환한다.
텍스처를 등록했다면, 해제하는 법도 있어야 한다. 이는 좀더 쉽다.
bool Unload(const std::string& filePath)
{
auto it = resources.find(filePath);
if (it == resources.end())
{
return false;
}
delete it->second;
resources.erase(it);
return true;
}
load 때와 같이 파일명을 키 값으로 받는다.
변수 it 은 원래 std::unordered_map<std::string, T*> ::iterator 와 같은 데이터형을 가지는데, 이렇게 길땐 auto를 사용해주면 컴파일러가 알아서 맞춰준다. iterator 는 자료 구조를 순회하면서 접근하는 반복자 라는 뜻이다.
auto it = resources.find(filePath); 면 it 변수가 resources를 돌며 filePath에 해당하는 요소를 찾아서 그 반복자를 갖고, 없으면 end가 된다.
if (it == resources.end())
{
return false;
}
그 요소가 없다면 언로드가 실패가 되고,
delete it->second;
resources.erase(it);
return true;
요소가 있다면 그 요소의 값, first는 파일주소이니 실제 메모리를 할당한 second 를 해제하고 resources에서 그 요소를 지워준다. 그후 성공을 리턴한다.
만일 여러개의 리소스가 로드되어 있다면, 소멸자에서 모든 리소스를 한번에 해제하는 unloadAll 함수를 만들어 넣어주는것으로 해결할 수 있다.
void UnloadAll()
{
for (const auto& pair : resources)
{
delete pair.second;
}
resources.clear();
}
여기서의 auto도 반복자가 모든 요소를 순회하며 메모리를 전부 해제하고, 마지막으로
resources를 비워준다.
로드된 리소스를 사용하기 위해 Get 함수를 만들어준다.
T* Get(const std::string& filePath)
{
auto it = resources.find(filePath);
if (it == resources.end())
{
return nullptr;
}
return it->second;
}
이전과 동일하다. 만약 없다면 nullptr 를 리턴하고, 있으면 그 데이터값을 반환한다.
이제 만든 리소스 매니저가 잘 돌아가나 시험해보자.
ResourceMgr<sf::Texture> texMgr;
texMgr.Load("graphics/player.png");
sf::Sprite player;
player.setTexture(*texMgr.Get("graphics/player.png"));
Texture 데이터형을 다루는 texMgr 를 생성한다.
그후 로드와 겟 함수를 통해 Timber 게임에서 사용했던 player 텍스처를 등록하고 그려본다.
resMgr.Unload("graphics/player.png");
반드시 unload를 메인함수 종료 전에 해야 한다.
이 리소스 매니저의 아쉬운 점이라면 전역 변수, static을 사용하지 않아 다른 함수에서 load unload에 접근할 수 없다는 점이다. 이를 해결하기 위해 싱글턴 이라는 기능을 리소스 매니저에 적용해 보자.
싱글턴이란 생성자가 여러번 호출되더라도 최초 생성된 객체 하나만 있고, 나머지 생성자도 최초 생성된 객체를 반환하는 형태이다. 즉 전역 변수처럼 사용할 수 있는것이다.
template <typename T>
class Singleton
{
protected:
Singleton() = default;
virtual ~Singleton() = default;
public:
static T& Instance()
{
static T instance;
return instance;
}
Singleton(const Singleton&) = delete;
Singleton(Singleton&&) = delete;
Singleton& operator=(const Singleton&) = delete;
Singleton& operator=(Singleton&&) = delete;
};
static으로 인스턴스를 생성했기 때문에 전역 변수처럼 기동한다.
이제 리소스 매니저 클래스가 이 싱글턴 클래스를 상속받으면 된다.
class ResourceMgr : public Singleton<ResourceMgr>
싱글턴 뒤의 <ResourceMgr> 는 리소스 매니저 인스턴스의 T를 싱글턴 클래스의 T에 입력한다는 문법이다. 따라서 리소스 매니저 인스턴스, 즉 텍스처 매니저 폰트 매니저 등등을 만들어도 각각이 싱글턴으로 적용된다.
이제 어떤 함수를 만들어도
ResourceMgr& mgr = ResourceMgr::Instance();
처럼 리소스 매니저를 Instance 메소드를 통해 호출하면
mgr.Load("example.png");
mgr.Unload("example.png");
처럼 사용할 수 있다.