SDL 用法,第 2 部分:"Pirates Ho!" 编码
新游戏编码的首要步骤
Sam Lantinga 和 Lauren MacDonell
Loki Entertainment Software,首席程序员
技术作家
2000 年 3 月
|
为什么使用 C++ |
automake 和 autoconf |
错误处理 |
对象缓存 |
日志记录函数 |
重新开发代码 |
组装起来 |
结束语 |
参考资料 |
关于作者 |
上个月,Sam Lantinga 和 Lauren MacDonell 开始了 "Pirates Ho!" 的初始编码和图形设计。在创造这个冒险探宝和角色扮演游戏的日记的这部分中,作者示范了使用 C++ 和各种开放源码工具为游戏编码的首要步骤。Sam 还讨论了对象缓存、错误处理和日志记录函数等内容。
在关于新游戏 "Pirates Ho!" 系列文章的这一部分中,我们将讨论使用 C ++ 和介绍几种帮助创建一些所需文件的 GNU 工具,来进行游戏的初始编码。然后继续讨论错误处理、对象缓存、日志记录函数,最后还提出了如何进一步进行的几项说明。
为什么使用 C++
在 Loki,大多数游戏都使用 C 或 C++,其中也带有少数速度关键型例程,但游戏可以并且已经使用任何语言编写。我决定使用 C++ 是因为在设计过程中它适合于以面向对象方式思考游戏逻辑,而且因为我习惯于使用 C++(请参阅“侧栏”)。
静态 C++ 对象 静态 C++ 对象非常适合用作整个游戏生命期中的全局对象,但使用它们仍有一些缺陷。静态 C++ 对象的构造函数一般在 main 函数之前运行,但是构造函数和析构函数的运行次序却没有指定。这就意味着如果它们依赖于任何外部条件,则它们也许不会像预期的那样运行。 在动态装入的共享对象中使用静态 C++ 对象甚至更危险,因为卸装共享对象时,gcc (2.95.2) 的当前实现不会调用析构函数。析构函数却在退出时调用,这将导致崩溃,因为析构函数的代码已不再可用。 |
C++ 在不同的平台上受到了广泛支持,但仍需注意所使用的 C++ 语言特性,因为不同的编译器可能会,也可能不,实现或强制全部 ANSI C++ 规范。在开发 "Pirates Ho!" 的过程中,我们将在 Linux 上使用 g++、在 Windows 上使用 Visual C++ 进行编译,而在 MacOS 上使用免费的 MPW 工具。
automake 和 autoconf,Linux 方式
automake 和 autoconf 是一组 GNU 工具,它们允许针对不同的编译环境自动配置源码。SDL 提供 m4 宏,因此可以支持 autoconf 过的应用程序。这种宏允许配置脚本检测适当版本的 SDL 是否在系统上可用。
要创建使用 automake 和 autoconf 的新应用程序,我们遵循以下 6 个步骤:
- 创建一个 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 ])
- 创建 acinclude.m4。GNU aclocal 工具从文件 acinclude.m4 中读取一组它使用的宏,然后将这些宏合并到文件 aclocal.m4,autoconf 使用这个文件生成 "configure" 脚本。
对于我们这种情况,我们只想添加对 SDL 的支持,因此我们将 SDL 发行版中的 "sdl.m4" 文件复制到 acinclude.m4 中。如果安装了 SDL-devel rpm,那么通常可以在 /usr/share/aclocal 目录中找到 "sdl.m4" 文件。
- 创建 Makefile.am。GNU automake 工具使用 Makefile.am 文件来描述用于创建程序的源码和库。用户运行配置脚本时,它使用该描述来生成转换成 makefile 的 Makefile.in 模板。
对于我们这种情况,我们有一个顶级目录,它包含 README、一些文档和脚本,还有一个包含游戏实际源码的子目录 "src"。
顶层的 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
这里,我们告诉 automake 我们正在构建的程序叫做 "pirates",它由大量的源文件组成。automake 用内置规则来构建 C++ 源文件,并将它们放到生成的 makefile 中,这样我们就不必担心它,可以集中精力编写有用的代码。
- 自举配置进程:automake 需要复制一些辅助文件,它使用这些文件来检测当前的体系结构等情况。要自举 automake,重新运行 "automake -a -v -c --foreign" 直到它不再告诉您它正在复制文件为止。
- 创建 autogen.sh:这一步骤是可选的,但为您提供了一种方法,可以只用一个简单步骤就重新生成所有与配置相关的文件,然后为当前构建环境配置软件。我们使用以下脚本:
-
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 */ |
这个类提供了一种方法来存储对象的状态,并将该状态传播到将消息打印到屏幕的那一层中。例如,当从文件装入子画面对象定义时,装入函数也许会用图像文件的名称来调用图像构造函数。如果图像对象不能装入文件,那么它将状态设置为 STATUS_ERROR
,并设置将传播到顶层的错误消息:
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()); } |
在我们的游戏中大量使用了这种错误处理代码。
对象缓存
对于共享的图像和同步播放的声音样本等,我早就意识到需要几种缓存算法。我决定编写一个通用的资源管理器,它可以缓存对游戏中被多次同时使用的所有对象的访问。
第一步是要为所有可缓存对象创建一个通用基类,这样就可以在高速缓存中操作这些对象,而不必确切知道对象的类型:
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 */ |
所有可缓存对象有一个与它们相关联的引用计数,因此每次使用它们时,这个计数就会增加,每次释放它们时,计数就会减少。在本实现中,当引用计数减到零时,对象就释放它本身。这就允许创建对象,并将它传送到引用该对象的函数中,然后由创建程序释放该对象,但只在不再使用对象时创建程序才释放它。
如果某个对象意外地释放了多次,这种实现可能存在错误。以后我将在项目中添加代码以捕捉这种情况。基本上,我将保留一个单独的可缓存对象池,并记录上一次对象释放的堆栈跟踪信息,在检测到某个对象被再次释放时发出一个俘获信号。在 Linux 上可以通过使用一组包含在 glibc 2.0 和更新版本中的函数来实现这种操作,因为它们可以让您从应用程序中记录和打印堆栈跟踪信息。有关详细信息,请参阅 /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"); } } }; |
使用 printf() 进行调试 您手边最强大的调试工具之一就是 printf()。我喜欢在代码中使用 printf(),而不用 cout,因为它是标准 I/O 库的部件,并且不会依赖 iostream,而 iostream 在不同平台上的实现也不同。 当捕捉到一个细微的问题时,我经常发现在查找到问题根源之前代码已经经过了许多复杂的步骤。我试着在代码的可疑区域内到处插入 printf() 语句,以了解正在执行什么,以及何时执行。当交叉编译到 Win32 时,通常这是调试问题的唯一方法。 |
这个实现相对比较简单。我使用模板是因为该缓存对象将与几种不同类型的数据一起使用,如图像、音乐、声音等。可缓存数据保存在一个单向链表中,并且如果所请求的对象不在高速缓存中,那么将动态地装入该对象。当不再需要数据时,不会立即释放它,而是释放到通用池中以备不久的将来又要使用它。
高速缓存有一个无用信息收集函数,它可以释放所有不使用的对象。在开发时,我使用它来检测代码中是否有遗漏的对象。当执行无用信息收集时,可以遍历剩余对象的列表以确保没有留下应该释放的对象。
日志记录函数
各种日志记录函数是非常有用的,它们可以打印调试消息、向用户显示警告等等。我开发了一组在游戏中大量使用的函数。以后,我们可能需要更好地控制打印的内容以及打印时间。例如,我们也许要打印对象缓存信息,而不要打印窗口小部件构造函数/析构函数日志,等等。
以下就是我们所使用的代码:
/* 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 } |
请注意 varargs 的使用:va_start()、vsprintf()、va_end()。这让我们可以在记录中使用类似于 printf 的功能,允许以下的语句:
log_warning("Only %d objects left in cache", numleft);
这些函数可以扩展成弹出对话框,或者记录到文件中,以及打印到标准错误输出。我们将了解将来游戏还需要些什么功能。
重新开发代码
许多情况下,如果要开发代码,最好使用现有代码。每当开始对项目进行编码时,首先应该在网络上搜索其它类似的项目。您也许会发现根本不必做任何工作 -- 已经有人编写了您需要的代码。
网络上的 Freshmeat(请参阅参考资料)是个好去处。许多开放源码项目都将它们的项目列在 Freshmeat 上。
对于我们这种情况,我们要求代码装入各种图像格式,还要求代码装入并播放声音和音乐。由于本文着重于使用 SDL 设计游戏,因此我们将寻找已经设计好的代码来使用所选择的库。
我们使用 SDL_image 库来装入图像;使用 SDL_mixer 库来装入并播放音频剪辑和音乐(请参阅参考资料)。
还需要一个简单的单向链表实现,因此我们使用 SGI 发行的“标准模板库”(或者简称 STL)(请参阅参考资料)。
“标准模板库”在不同平台有不同的实现,因此我们也许会在最后的游戏中最终使用其它库,但现在它提供了一组很好的基本实用程序对象。
组装起来
现在,我们有了所有的基本部件,可以开始在屏幕上放一些东西。我们有了用于装入图像和音乐的代码,以及显示到屏幕的代码,以及将这些代码结合到一起的接口。我们添加了一个样本闪屏,Nicholas Vining 谱写了样本主题音乐。以下就是结果:
样本闪屏
您需要安装上面提到的库来运行参考资料中的二进制样本程序。
结束语
我们已经开始了游戏的初始编码,效果很好。到目前为止,我们可以播放初始的动画序列,并且已经完成游戏所需的一些基本架构。下个月,我们希望有第一个版本的海战游戏可以进行测试,并且有许多船只和风景的图片。
参考资料
- 请访问 Pirates Ho! 网站
- 可以下载该样本的源码和二进制码:
- "Pirates Ho!" 使用的库
- “SDL:使 Linux 变得有趣”,就在 developerWorks 上:Sam 向您介绍了 SDL,以及它是如何工作的
- “使用 SDL:"Pirates Ho!" 的诞生”,也在 developerWorks 上:Sam 和 Lauren 发表的本系列的第一篇文章,讲述了他们如何设计游戏概念
- An introduction to the SDL API
更多的游戏开发资源:
- Freshmeat,一个很好的开放源码项目来源
- 各种软件下载(各种价格)
- 一个组织得很好的 Windows、Mac 和 Linux 图形、编程和其它软件包集合
- 用于三维设计的软件:
-
- Canoma:用于根据扫描的照片或数字照片快速创建逼真的三维模型,但不必具备很多的三维技术
- The SCiTech graphics library
- 下载一些 Linux 上最好的游戏:
- Game developer's newsgroup
- A review of MythII: Soulblighter,在 LinuxWorld 杂志中
- "Games users play",在 LinuxWorld 杂志中
- "A whole new civilization built on Linux",在 LinuxWorld 杂志中
关于作者
Sam Lantinga 是 Simple DirectMedia Layer (SDL) 库的作者,现在是 Loki Entertainment Software 的首席程序员,这家公司致力于生产最畅销的 Linux 游戏。他与 Linux 和游戏打交道是从 1995 年开始的,从事各种 DOOM! 工具移植,以及将 Macintosh 游戏 Maelstrom 移植到 Linux。
Lauren MacDonell 是 SkilledNursing.com 的一位技术作家,也是 "Pirates Ho!" 的合作开发者。在工作、写作或跳舞之余,她照管着她的热带鱼。