리소스 매니저 구성 |
| programming/general 2003/01/13 00:44 |
간단히 하위 레벨의 게임 리소스 매니징에 대해서, 간단히 적어보겠습니다. (엄밀히 얘기하면 리소스 매니징 중에 파일 매니징이라고 해야 하겠습니다.) 내용이 일반적인 것들이라 말이 좀 꼬여 있어도 의미는 통할 것이라 생각합니다. ^^
1. 리소스 매니저
일반적인 상용급 게임들의 파일 구성을 보면 데이타 파일들인 몇개의 파일에 묶인 상태로 배포하는 것을 볼 수 있습니다. (물론 파일이 몇 천 개까지 인스톨되는 게임도 볼 수는 있습니다)
이런 방식은 "게임 리소스가 그대로 노출되지 않는 다" 것 외에도 여러 가지로 장점이 있습니다. (실제 하드를 적게 먹기도 하고... 멋있기도... ^^)
이런 식으로 파일을 묶어서 사용하려고 시도하는 과정에서, 자연스럽게 파일 I/O 루틴을 통합해서 사용하게 되기 때문에, 내부적으로는 압축이나 암호화등의 작업을 일괄적으로 하게 응용할 수 있습니다.
(각 리소스 로딩 루틴에 암호화 루틴이나 압축 루틴을 넣을 필요가 전혀 없기 때문에 매니저 차원에서 암호화가 된다면 어렵게 암호화해서 읽는 루틴에 넣을 필요가 없습니다. - 예를 들어 설정 파일로 csv포맷을 사용하더라도 외부에 노출될 까봐 바이너리로 바꾸거나 할 필요는 없을 것입니다. 개발상의 불편함이 줄어들 수 있을 것입니다.)
2. 기본 코드 구성
가볍게 눈에 들어올만한 코드들 몇 개의 작성 예를 들어보도록 하겠습니다.
(코드의 효율성에 대한 것은 접어두시고 그냥 눈으로 한번 보시면 될거 같습니다.)
먼저 파일의 기본 클래스를 아래와 같이 정의했습니다. (일반적으로 게임에서는 읽기에만 최적화한다고 보고, 그냥 read 와 seek 만 구현하겠습니다.)
| class File { public : virtual ~File() {} virtual int Read(void *ptr, int len) = 0; enum fileorigin { SET, CUR, END } ; virtual void Seek(int pos, fileorigin origin) = 0; } ; |
이 형에 맞춰서 아주 기본적인 FILE IO 루틴을 붙여보겠습니다. (그냥 단순히 fread, fseek 등을 wrap 한 클래스입니다.)
| class FileStream : public File { public : FileStream(FILE *fp) { m_fp = fp; } ~FileStream() { fclose(m_fp); } int Read(void *ptr, int len) { return fread(ptr, 1, len, m_fp); } void Seek(int pos, fileorigin origin) { switch(origin) { case CUR : fseek(m_fp, pos, SEEK_CUR); break; case END : fseek(m_fp, pos, SEEK_END); break; case SET : fseek(m_fp, pos, SEEK_SET); break; } } private : FILE * m_fp; } ; |
읽을 때는 아래처럼 하면 되겠네요.
| File * OpenFile(const char *fname) { FILE *fp; fp = fopen(fname, "rb"); if (fp) return new FileStream(fp); return 0l; } |
실제로 묶인 파일의 정보를 가지고 있으면 이 클래스를 이용해서 아래의 루틴처럼 간단하게 파일의 일부분을 이용하는 루틴을 작성할 수 있을 것입니다.
| class FileStreamPacked : public File { public : FileStreamPacked(File *fp, int start, int len) { m_stream = fp; m_pivot = start; m_pos = 0; m_len = len; } int Read(void *ptr, int len) { if (len + m_pos > m_len) len = m_len - m_pos; if (len > 0) { m_stream->Seek(m_pivot + m_pos, SET); m_stream->Read(ptr, len); } m_pos += len; return len; } void Seek(int pos, fileorigin origin) { switch(origin) { case CUR : m_pos += pos; break; case END : m_pos = m_len + pos; break; case SET : m_pos = pos; break; } } private : File * m_stream; int m_pivot, m_len, m_pos; } ; |
아래처럼 메모리 내용을 파일처럼 읽도록 하는 것도 구성할 수 있겠습니다. (추가적인 작업없이 실행파일에 사용자 리소스로 데이타 넣어서 사용하는 것도 가능하겠네요.)
| class Filebymem : public File { public : Filebymem(void *mem, int len, bool free=true) { m_ptr = (unsigned char*) mem; m_offset = 0; m_len = len; m_free = free; } ~Filebymem() { if (m_free) delete [] m_ptr; } int Read(void *ptr, int len) { if (len + m_offset > m_len) len = m_len - m_offset; if (len > 0) memcpy(ptr, m_ptr + m_offset, len); m_offset += len; return len; } void Seek(int pos, fileorigin origin) { switch(origin) { case CUR : m_offset += pos; break; case END : m_offset = m_len + pos; break; case SET : m_offset = pos; break; } } private : unsigned char *m_ptr; int m_offset, m_len; bool m_free; } ; |
이렇게 하나씩 확장해 나가면 어렵지 않게 원하시는 것에 접근 하실 수 있을 것입니다.
3. 구성 예제
예제는 간단하게 zlib 를 이용해서 zip 파일에서 리소스를 읽도록 작성해 본 셈플입니다.
zlib 는 강력한 압축 라이브러리로 홈페이지에 가시면 자세한 정보를 얻으실 수 있습니다.
zlib 홈페이지 URL : http://www.gzip.org/zlib
소스에 포함된 zlib 는 제가 임의로 TrueCoverage 같은 툴 사용해서 unzip 하는 부분만 골라낸 것이므로 참고만 하시고, 안정적으로 사용하고자 하실 때는 사이트에서 최신 버전을 받으셔서 사용하세요. ^^
리소스의 최적된 형태가 zip 파일이 아닐까 하는 생각이 드네요. (물론 여러가지로 게임 환경에 맞춰서 더욱 최적화할 필요는 있겠습니다만...)
예제는 아주 기본적인 루틴만 붙여 놓은 수준이고, 실제 게임에서 사용한다면 파일 캐싱, 버퍼링등의 기능부터 파일 대소문자 무시하는 기능이나 파일 정보들을 리스트로 만들어 빠르게 검색하는 것등 자잘한 것들까지도 고려해서 작성하셔야 합니다.
(멀티 쓰레드 모드에서는 파일 하나에 동시에 접근 할 경우에 대한 대비도 해두어야 합니다.)
4. 응용
기본적으로 파일 매니징을 구현하면서 몇 가지 간단한 처리한 해주면 개발상 이득을 얻을 수 있습니다.
제가 작업했던 게임들에서 시행 착오 겪으면서 해결책을 찾았던 것들 몇 가지 적어보면
- 파일 읽기 우선 수위 두기
먼저 실제 경로에서 파일을 체크해서 있으면 읽고, 만약 없다면 설정된 패킹 파일에서 파일을 읽도록 작업하면 개발할 때 몇 가지 이 점이 있습니다.
작업자가 리소스를 변경하고자 할 때, 그냥 해당 작업 파일을 해당 경로에 카피하고 게임을 실행하면 됩니다.
기본적으로 추가 파일 없이 묶인 파일만 두고 작업하다가 필요할 경우 추가해주고, 일정 주기로 이 파일들을 업데이트하면 되기 때문에 관리가 편리합니다.
기획자의 작업 하드에는 몇 천 개의 사운드, 그래픽 파일이 없어도 상관이 없겠죠.
- 묶인 파일에 우선 순위 두기
묶인 것에서 중에서 우선 순위를 두어 읽게 되면 게임에 리소스만 업데이트 되는 경우에는 해당 경로에 카피만 하면 자동으로 업데이트된 파일을 읽게 되는 효과를 얻을 수 있습니다. (패치나 업데이트를 고려해서 최신 파일부터 검색하도록 우선 순위를 두면 되겠네요. - 묶인 파일 이름으로 정렬하면 편리합니다.)
- 이름으로 경로 찾기
게임을 개발하다보면 리소스가 몇 천 개까지도 불어나기 때문에 작업할 때 체계적인 계층구조로 리소스를 관리하게 됩니다. 이러다 보면 리소스 경로를 바꿔야 할 일이 종종 생기는 데, 실수로 아주 치명적인 문제가 발생하기도 합니다. 불필요하게 중복된 파일들이 많아지기도 합니다.
여러 가지 해법이 있겠지만 게임을 시작할 때 모든 파일의 "파일이름, 풀경로" 리스트를 만들어서 읽고자 하는 파일의 이름만으로 리소스에 접근할 수 있게 하면 편리합니다. LoadObj("monster/orc/orcgreelv2.psk") 이런 식이 아니라 LoadObj("orcgreelv2.psk") 이런 식으로 접근하게 되면 여러 가지로 편리하게 작업을 할 수 있습니다. 단, 중복된 파일 이름을 가지는 리소스가 없어야 할 것입니다. (문제되는 파일이 있으면 경고하는 식으로 개발자가 미리미리 체크하지 않으면 나중에 골치 아파질 수도 있습니다.)
5. 끝
크게 난이도가 있는 파트는 아니지만 신경 써주면 여러 가지로 할 것들이 많은 분야라고 생각됩니다.
(업데이트가 중요한 온라인 게임이나 파일 배치가 중요한 콘솔용 게임, 로딩 타임이나 용량이 중요한 게임들에는 해당 포인트에 맞춘 추가적인 작업들이...)
파일 레벨의 하위 리소스 관리 루틴은 플렛폼과 장르에 상관없이 재활용이 가능한 분야라서, 저는 처음 만든 게임부터 시작해서 일한 회사에서 나온 게임들에까지 별다른 추가 작업없이 엄청난 우려먹기를 할 수 있었습니다.
ps. 이런 류의 솔류션도 가지고 있다면 나름대로의 개발자로서의 가치를 높일 수 있을 지도... (노력대비 효과가... ^^)
unzip4game.zip
댓글을 달아 주세요
너는 아주 좋은 보는 위치가 있는다!
아주 좋은 위치 나는 그것을 감사 좋아한다!