[转载]SDL 用法,第 2 部分:"Pirates Ho!" 编码

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++ 语言特性,因为不同的编译器可能会,也可能不,实现或强制全部 ANSI C++ 规范。在开发 "Pirates Ho!" 的过程中,我们将在 Linux 上使用 g++、在 Windows 上使用 Visual C++ 进行编译,而在 MacOS 上使用免费的 MPW 工具。

automake 和 autoconf,Linux 方式
automake 和 autoconf 是一组 GNU 工具,它们允许针对不同的编译环境自动配置源码。SDL 提供 m4 宏,因此可以支持 autoconf 过的应用程序。这种宏允许配置脚本检测适当版本的 SDL 是否在系统上可用。

要创建使用 automake 和 autoconf 的新应用程序,我们遵循以下 6 个步骤:

  1. 创建一个 README 文件。autoconf 以后会使用这个文件来验证源码发行版是否完整。这对用户也有帮助。

  2. 创建 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
    ])
    


  1. 创建 acinclude.m4。GNU aclocal 工具从文件 acinclude.m4 中读取一组它使用的宏,然后将这些宏合并到文件 aclocal.m4,autoconf 使用这个文件生成 "configure" 脚本。

    对于我们这种情况,我们只想添加对 SDL 的支持,因此我们将 SDL 发行版中的 "sdl.m4" 文件复制到 acinclude.m4 中。如果安装了 SDL-devel rpm,那么通常可以在 /usr/share/aclocal 目录中找到 "sdl.m4" 文件。

  2. 创建 Makefile.am。GNU automake 工具使用 Makefile.am 文件来描述用于创建程序的源码和库。用户运行配置脚本时,它使用该描述来生成转换成 makefile 的 Makefile.in 模板。

    对于我们这种情况,我们有一个顶级目录,它包含 README、一些文档和脚本,还有一个包含游戏实际源码的子目录 "src"。

    顶层的 Makefile.am 文件非常简单。它只告诉 automake 有一个子目录包含了需要读取的 Makefile.am 文件,并给出了一个列表,该列表列出了在构建源码档案文件时需要添加到发行版中的其它文件:

  3. 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 中,这样我们就不必担心它,可以集中精力编写有用的代码。

  4. 自举配置进程:automake 需要复制一些辅助文件,它使用这些文件来检测当前的体系结构等情况。要自举 automake,重新运行 "automake -a -v -c --foreign" 直到它不再告诉您它正在复制文件为止。

  5. 创建 autogen.sh:这一步骤是可选的,但为您提供了一种方法,可以只用一个简单步骤就重新生成所有与配置相关的文件,然后为当前构建环境配置软件。我们使用以下脚本:
  6. 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");
        }
    }
};


这个实现相对比较简单。我使用模板是因为该缓存对象将与几种不同类型的数据一起使用,如图像、音乐、声音等。可缓存数据保存在一个单向链表中,并且如果所请求的对象不在高速缓存中,那么将动态地装入该对象。当不再需要数据时,不会立即释放它,而是释放到通用池中以备不久的将来又要使用它。

高速缓存有一个无用信息收集函数,它可以释放所有不使用的对象。在开发时,我使用它来检测代码中是否有遗漏的对象。当执行无用信息收集时,可以遍历剩余对象的列表以确保没有留下应该释放的对象。

日志记录函数
各种日志记录函数是非常有用的,它们可以打印调试消息、向用户显示警告等等。我开发了一组在游戏中大量使用的函数。以后,我们可能需要更好地控制打印的内容以及打印时间。例如,我们也许要打印对象缓存信息,而不要打印窗口小部件构造函数/析构函数日志,等等。

以下就是我们所使用的代码:

/* 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 谱写了样本主题音乐。以下就是结果:

样本闪屏
样本闪屏

您需要安装上面提到的库来运行参考资料中的二进制样本程序。

结束语
我们已经开始了游戏的初始编码,效果很好。到目前为止,我们可以播放初始的动画序列,并且已经完成游戏所需的一些基本架构。下个月,我们希望有第一个版本的海战游戏可以进行测试,并且有许多船只和风景的图片。

参考资料

更多的游戏开发资源:

关于作者
Sam Lantinga 是 Simple DirectMedia Layer (SDL) 库的作者,现在是 Loki Entertainment Software 的首席程序员,这家公司致力于生产最畅销的 Linux 游戏。他与 Linux 和游戏打交道是从 1995 年开始的,从事各种 DOOM! 工具移植,以及将 Macintosh 游戏 Maelstrom 移植到 Linux。

Lauren MacDonell 是 SkilledNursing.com 的一位技术作家,也是 "Pirates Ho!" 的合作开发者。在工作、写作或跳舞之余,她照管着她的热带鱼。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值