이번 작업에는C++을 사용하고 필요한 파일을 생성하는 데 도움이 될 여러 GNU 툴을 소개하면서 게임의 초기 코드 작업에 착수할 것이다. 그런 다음 에러 처리, 객체 캐시, 로그 기능에 대해 살펴보고 거기서 몇 가지 참고 사항을 검토할 것이다.
Loki에서 대부분의 게임은 속도 중심 루틴을 위해 약간의 어셈블리와 함께 C 또는 C++을 사용하여 제작되지만 다른 언어로도 제작될 수 있다. C++이 설계 과정 동안 객체 중심 방식에서 게임 논리를 생각하는 데 알맞고 C++에 익숙하기 때문에 C++를 사용하기로 결정했다(사이드바 참조).
서로 다른 컴파일러에서 ANSI C++ 사양 전체를 구현 또는 구현하지 않거나 실행하기 때문에 사용자는 자신이 사용하는 C++ 언어 기능에 대해 주의를 해야 하기는 하지만, C++은 여러 플랫폼에서 폭 넓게 지원되고 있다. Pirates Ho! 제작 과정에서 MacOS에서는 무료 MPW 툴을 사용하고 Linux에서는 g++로 컴파일하고 Windows에서는 Visual C++로 컴파일할 것이다.
Automake 및 autoconf는 다른 컴파일 환경에 대해 소스 코드를 자동으로 구성할 수 있게 해주는 GNU 툴 세트이다. SDL은 m4 매크로를 제공하여 autoconf되는 어플리케이션을 지원한다. 이 매크로를 사용하면 구성 스크립트는 시스템에서 알맞은 버전의 SDL를 사용할 수 있는지의 여부를 감지할 수 있다.
Automake 및 autoconf를 사용하는 새로운 어플리케이션을 생성하려면 다음 여섯 단계를 수행한다 :
- README를 생성한다. 이 파일은 소스 배포가 완료되었는지 확인하기 위해 autoconf에서 나중에 사용한다.
- configure.in을 생성한다. GNU autoconf 툴은 configure.in이라는 구성 파일을 읽는데, 이 파일은 시스템에서 찾아야 하는 기능 및 이 정보를 사용하여 생성하는 데 필요한 출력 파일에 대해 알려준다. 다음은 제작 중인 게임의 configure.in 파일이다 :
configure.in:
# This first line initializes autoconf and gives it a file that it can
# look for to make sure the source distribution is complete.
AC_INIT(README)
# The AM_INIT_AUTOMAKE macro tells automake the name and version number
# of the software package so it can generate rules for building a source
# archive.
AM_INIT_AUTOMAKE(pirates, 0.0.1)
# We now have a list of macros which tell autoconf what tools we need to
# build our software, in this case "make", a C++ compiler, and "install".
# If we were creating a C program, we would use AC_PROC_CC instead of CXX.
AC_PROG_MAKE_SET
AC_PROG_CXX
AC_PROG_INSTALL
# This is a trick I learned at Loki - the current compiler for the alpha
# architecture doesn't produce code that works on all versions of the
# alpha processor. This bit detects the current compile architecture
# and sets the compiler flags to produce portable binary code.
AC_CANONICAL_HOST
AC_CANONICAL_TARGET
case "$target" in
alpha*-*-linux*)
CXXFLAGS="$CXXFLAGS -mcpu=ev4 -Wa,-mall"
;;
esac
# Use the macro SDL provides to check the installed version of the SDL
# development environment. Abort the configuration process if the
# minimum version we require isn't available.
SDL_VERSION=1.0.8
AM_PATH_SDL($SDL_VERSION,
:,
AC_MSG_ERROR([*** SDL version $SDL_VERSION not found!])
)
# Add the SDL preprocessor flags and libraries to the build process
CXXFLAGS="$CXXFLAGS $SDL_CFLAGS"
LIBS="$LIBS $SDL_LIBS"
# Finally create all the generated files
# The configure script takes "file.in" and substitutes variables to produce
# "file". In this case we are just generating the Makefiles, but this could
# be used to generate any number of automatically generated files.
AC_OUTPUT([
Makefile
src/Makefile
]) |
- include.m4를 생성한다. GNU aclocal 툴은 acinclude.m4 파일에서 사용하는 매크로의
리스트를 읽고 이 내용을 "configure" 스크립트 생성을 위해 autoconf에 의해 사용되는 aclocal.m4 파일에
병합시킨다.
우리는 단지 SDL에 대한 지원을 추가하고자 하였으므로 SDL 배포판에 포함되어있는 "sdl.m4" 파일을 acinclude.m4에 복사하였다. "sdl.m4" 파일은 일반적으로 /SDL-devel rpm이 설치되어 있는 경우, usr/share/aclocal 디렉토리에 있다.
- Makefile.am을 생성한다. GNU automake 툴은 Makefile.am 파일을 프로그램 생성에
사용되는 소스와 라이브러리를 설명한다. GNU automake 툴은 이 설명으로 사용자가 구성 스크립트를 실행할 때 makefiles로
변경되는 Makefile.in 템플릿을 만든다.
우리의 경우에서 보자면, 최상위 디렉토리에는 README 파일, 일부 문서 및 스크립트가 들어 있고, 하위 디렉토리 "src"에는 게임의 실제 소스 코드가 들어 있다.
최상위 Makefile.am 파일은 매우 간단하다. Makefile.am 파일은 automake에게 읽어야 할 Makefile.am 파일이 들어 있는 하위 디렉토리가 있다는 것을 알려주고, 소스 아카이브를 생성할 때 배포판에 추가해야 할 여분 파일의 리스트를 제공한다 :
Makefile.am
SUBDIRS = src EXTRA_DIST = NOTES autogen.sh |
소스 Makefile.am에는 실제 요점 부분이 포함되어 있다. :
src/Makefile.am
bin_PROGRAMS = pirates pirates_SOURCES = \ cacheable.h game.cpp game.h image.cpp image.h logging.cpp logging.h \ main.cpp manager.h music.cpp music.h nautical_coord.h paths.cpp \ paths.h screen.h screen.cpp ship.h splash.cpp splash.h sprite.cpp \ sprite.h status.cpp status.h text_string.h textfile.h widget.h \ widget.cpp wind.h |
여기에서 우리가 생성 중인 프로그램이 "pirates"라는 것을 automake에게 명시하며, 프로그램은 많은 소스 파일로 구성되어 있다. Automake은 C++ 소스 파일 생성을 위한 built-in 규칙을 가지고 있으며, 이 규칙을 생성된 makefile에 입력하게 되므로 걱정하지 않고 올바른 코드를 생성하는 작업에만 중점을 둘 수 있다.
autogen.sh
#!/bin/sh # aclocal automake --foreign autoconf ./configure $* |
최초에 작업을 시작했을 때 먼저 생성한 것은 객체의 상태를 처리하기 위해 기본 클래스이다. :
status.h
/* Basic status reporting class */
#ifndef _status_h
#define _status_h
#include "textstring.h"
typedef enum {
STATUS_ERROR = -1,
STATUS_OK = 0
} status_code;
class Status
{
public:
Status();
Status(status_code code, const char *message = 0);
virtual ~Status() { }
void set_status(status_code code, const char *fmt, ...);
void set_status(status_code code) {
m_code = code;
m_message = 0;
}
void set_status_from(Status &object) {
m_code = object.status();
m_message = object.status_message();
}
void set_status_from(Status *object) {
set_status_from(*object);
}
status_code status(void) {
return(m_code);
}
const char *status_message(void) {
return(m_message);
}
protected:
status_code m_code;
text_string m_message;
};
#endif /* _status_h */ |
autogen.sh를 생성한다.
bool Image::Load(const char *image)
{
const char *path;
/* Load the image from disk */
path = get_path(PATH_DATA, image);
m_image = IMG_Load(path);
free_path(path);
if ( ! m_image ) {
set_status(STATUS_ERROR, IMG_GetError());
}
return(status() == STATUS_OK);
}
bool Sprite::Load(const char *descfile)
{
...
m_frames = new Image *[m_numframes+1];
for ( int i=0; i<m_numframes; ++i ) {
m_frames[i] = new Image(imagefiles[i]);
// This function is in the Status base class, and copies
// the status any error message from the image object
if ( m_frames[i]->status() != STATUS_OK ) {
set_status_from(m_frames[i]);
}
}
return(status() == STATUS_OK);
} |
... 그런 다음 최상위 스프라이트에서 루틴을 로드한다 :
if ( ! sprite->Load(spritefile) ) {
printf("Couldn't load sprite: %s\n", sprite->status_message());
} |
우리가 제작한 게임에는 거의 이런 종류의 에러 처리 코드가 사용된다.
시작부터 나는 공유되는 이미지나 동시에 재생할 수 있는 사운드 샘플 등에 대해 일종의 캐시 알고리즘이 필요하다는 사실을 깨달았다. 따라서 2회 이상 동시에 사용될 수 있는 게임의 모든 객체에 대해 액세스를 캐시 할 수 있는 일반 리소스 관리자를 생성하기로 결정했다.
이를 위한 첫번째 작업은 어떤 타입의 객체인지 정확하게 알지 않아도 캐시 내부에서 조작될 수 있도록 모든 캐시 가능한 객체에 대해 전반적인 기본 클래스를 생성하는 것이었다 :
cacheable.h
/* This object can be cached in the resource manager */
#ifndef _cacheable_h
#define _cacheable_h
class Cacheable
{
public:
Cacheable() {
ref_cnt = 1;
}
virtual ~Cacheable() {}
void AddRef(void) {
++ref_cnt;
}
void DelRef(void) {
/* Free this object when it has a count of 0 */
if ( --ref_cnt == 0 ) {
delete this;
}
}
int RefCnt(void) {
return(ref_cnt);
}
private:
int ref_cnt;
};
#endif /* _cacheable_h */ |
캐시 가능한 모든 객체는 관련된 참조 카운트가 있어, 사용될 때마다 카운트는 증가되고 릴리스될 때마다 카운트는 감소된다. 이런 방법으로 객체는 참조 카운트가 0이 되면 자유롭게 된다. 이렇게 하면 객체가 생성되고 객체에 대한 참조가 있는 함수에 전달된 후 생성자에 의해 릴리스되는데, 더 이상 사용되지 않는 객체만 자유롭게 된다.
이런 방법은 객체를 실수로 여러 차례 자유롭게 만드는 버그가 발생할 수 있다. 이런 경우에 대비하여 나중에 프로젝트의 후반부 정도에서 코드를 추가할 것이다. 기본적으로 나는 캐시 가능한 객체의 개별 풀을 유지할 것이며, 마지막 객체 릴리스에 대한 스택 추적 정보를 기록하고 다시 릴리스 중인 객체를 감지하면 트랩 신호를 울릴 것이다. 이 작업은 어플리케이션 내에서 스택 추적 정보를 기록 및 인쇄할 수 있는 glibc 2.0 이상에 포함된 일련의 함수를 사용하여 Linux에서 수행될 수 있다. 상세한 내용은 /usr/include/execinfo.h를 참조한다.
캐시 가능한 객체의 소멸자는 가상이라는 점에 주의한다. 이는 기본 클래스가 자유로울 때 유도된 정확한 클래스 소멸자 호출을 위해 필요하다. 그렇지 않으면 기본 클래스 소멸자는 호출되며 유도된 클래스 주변에 남은 객체는 누출된다.
일단 캐시 가능한 객체가 생성되었으면 이러한 객체를 유지하기 위한 캐시가 필요하다 :
/* This is a data cache template that can load and unload data at will.
Items cached in this template must be derived from the Status class
and have a constructor that takes a filename as a parameter.
*/
template<class T> class ResourceCache : public Status
{
public:
ResourceCache() {
m_cache.next = 0;
}
~ResourceCache() {
Flush();
}
T *Request(const char *name) {
T *data;
data = 0;
if ( name ) {
data = Find(name);
if ( ! data ) {
data = Load(name);
}
if ( data ) {
data->AddRef();
}
}
return(data);
}
void Release(T *data) {
if ( data ) {
if ( data->RefCnt() == 1 ) {
log_warning("Tried to release cached object");
} else {
data->DelRef();
}
}
}
/* Clear all objects from the cache */
void Flush(void) {
while ( m_cache.next ) {
log_debug("Unloading object %s from cache",
m_cache.next->name);
Unload(m_cache.next->data);
}
}
/* Clear all unused objects from the cache
This could be faster if the link pointer wasn't trashed by
the unload operation...
*/
void GarbageCollect(void) {
struct cache_link *link;
int n_collected;
do {
for ( link=m_cache.next; link; link=link->next ) {
if ( link->data->RefCnt() == 1 ) {
Unload(link->data);
break;
}
}
} while ( link );
log_debug("Cache: %d objects garbage collected", n_collected);
}
protected:
struct cache_link {
char *name;
T *data;
struct cache_link *next;
} m_cache;
T *Find(const char *name) {
T *data;
struct cache_link *link;
data = 0;
for ( link=m_cache.next; link; link=link->next ) {
if ( strcmp(name, link->name) == 0 ) {
data = link->data;
break;
}
}
return(data);
}
T *Load(const char *file) {
struct cache_link *link;
T *data;
data = new T(file);
if ( data->status() == STATUS_OK ) {
link = new struct cache_link;
link->next = m_cache.next;
link->name = strdup(file);
link->data = data;
m_cache.next = link;
} else {
set_status_from(data);
delete data;
data = 0;
}
return(data);
}
void Unload(T *data) {
struct cache_link *prev, *link;
prev = &m_cache;
for ( link=m_cache.next; link; link=link->next ) {
if ( data == link->data ) {
/* Free the object, if it's not in use */
if ( data->RefCnt() != 1 ) {
log_warning("Unloading cached object in use");
}
data->DelRef();
/* Remove the link */
prev->next = link->next;
delete link;
/* We found it, stop looking */
break;
}
}
if ( ! link ) {
log_warning("Couldn't find object in cache");
}
}
}; |
이런 구현은 비교적 직접적이다. 템플릿을 사용하는데 그 이유는 이 캐시 객체가 이미지, 음악, 사운드 등의 여러 종류의 데이터에 사용되기 때문이다. 캐시 가능한 데이터는 단독으로 연결된 리스트에 있으며, 요청된 객체가 캐시에 없는 경우, 요청된 객체는 동적으로 로드된다. 데이터가 더 이상 필요 없게 되면 데이터는 즉시 자유롭게 되지만 다음에 사용할 수 있도록 일반 풀에 릴리스된다.
캐시는 사용되지 않은 모든 객체를 자유롭게 하는 가비지(garbage) 컬렉션 기능이 있다. 나는 이러한 디버깅 개발로 코드에서 객체 누출을 감지한다. 가비지 컬렉션을 수행할 때 남은 객체의 리스트를 주의 깊게 검토하여 자유롭게 되어야 하는 객체가 모두 자유로워 졌는지 확인할 수 있다.
디버그 메시지 인쇄, 사용자에 대한 경고 표시 등의 여러 로깅 함수를 갖는 것이 좋다. 나는 게임에 대해서는 여러 함수를 사용한다. 조만간 우리는 어떤 것이 인쇄 중이고 언제 인쇄되는지에 대해 보다 정밀한 통제가 필요하게 될 것이다. 예를 들어 위젯 생성자/소멸자 로드 등이 아닌 객체 캐시 정보를 인쇄해야 될 때가 있다.
다음은 우리가 사용하는 내용이다 :
/* Several logging routines */
#include <stdio.h>
#include <stdarg.h>
void log_warning(const char *fmt, ...)
{
va_list ap;
fprintf(stderr, "WARNING: ");
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fprintf(stderr, "\n");
}
void log_debug(const char *fmt, ...)
{
#ifdef DEBUG
va_list ap;
fprintf(stderr, "DEBUG: ");
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
fprintf(stderr, "\n");
#endif
} |
코딩 프로젝트를 시작할 때마다 제일 먼저 해야 할 일은 유사한 다른 프로젝트를 찾는 것이다. 추가 작업이 전혀 필요 없는 프로젝트를 찾을 수도 있다. 즉, 필요로 하는 코드가 이미 생성된 프로젝트일 수 있다.
Freshmeat에서 상세한 정보를 찾도록 한다(참고자료 참조). Freshmeat에는 프로젝트가 나열되어 있는 오픈 소스 프로젝트가 많이 있다.
여러 이미지 형식을 로드 하는 코드 및 사운드와 음악을 로드하고 재생할 코드가 필요했다. 이 글은 SDL로 게임을 설계하는 것에 중점을 두고 있으므로, 선택한 라이브러리와 작업하도록 설계되어 있는 코드를 찾아 보겠다.
이미지를 로드 하려면 SDL_image 라이브러리를 사용하고, 오디오 클립이나 음악을 로드 및 재생하려면 SDL_mixer 라이브러리를 사용한다(참고자료 참조).
또한 SGI에 의해 릴리스된 Standard Template Library(STL)로 변환하기 위해 단독으로 연결된 방법이 필요하다(참고자료 참조).
Standard Template Library는 다른 플랫폼에 여러 방법을 가지고 있으므로, 최종 게임에서는 다른 것을 사용하여 끝낼 수 있지만 현재는 정확한 기본 유틸리티 객체를 제공한다.
이제 화면에 입력하기 위한 모든 준비가 끝났다. 이미지와 음악을 로드 하는 코드, 화면에 재생시키는 코드 및 이 모두를 활용할 수 있는 방법을 배웠다. 다음은 Nicholas Vining에서 배포된 샘플 타이틀 음악의 샘플 스플래시 화면이다 :
샘플 스플래시 화면
위에 나온 라이브러리는 참고자료의 바이너리 샘플을 실행하기 위해 설치되어 있어야 한다.
지금 게임에서 초기 코드 작업을 시작했고 그 결과는 긍정적이다. 지금까지 초기 스플래시를 즐길 수 있으며 완성된 게임에 대한 기본 인프라스트럭처 일부를 가지고 있다. 다음 달에는 많은 선박과 배경 그림과 함께 첫번째 선박 전투 게임을 발표하여 테스트할 예정이다.
- developerWorks worldwide 사이트에서 이 기사에 관한 영어원문.
- Pirates Ho! 웹사이트
- 소스 및 바이너리 다운로드 :
- "Pirates Ho!" 의 라이브러리
- "SDL:
Making Linux fun", developerWorks: SDL 설명
- "Using
SDL: the birth of "Pirates Ho!" : developerWorks의 SDL 시리즈
- An
introduction to the SDL API
게임 개발 리소스 :
- Freshmeat : 오픈 소스 프로젝트 관련
정보
- A wide assortment of software
downloads : 소프트웨어 다운로드
- A nicely organized collection of graphics,
programming, and other software packages : Windows, Mac, Linux를
위한 자료
- 3-D 디자인을 위한 소프트웨어:
- The SCiTech graphics library
- Linux용 게임 다운로드:
- Game developer's
newsgroup
- A
review of MythII: Soulblighter : 게임 개발자 뉴스그룹
- "Games
users play" (LinuxWorld )
- "A
whole new civilization built on Linux"(LinuxWorld)
Sam Lantinga는 Simple DirectMedia Layer (SDL) 라이브러리의 작성자이며 현재 Linux용 게임 전문 회사인 Loki Entertainment Software의 프로그래머로 활동하고 있다. 그는 다양한 DOOM! 툴 포트와 Macintosh 게임인 Maelstrom를 Linux로 포트 시키면서 Linux와 게임관련 일을 하게 되었다.