SDL用法:"PiratesHo!"全集

SDL用法:"PiratesHo!"的诞生


Sam Lantinga, 首席程序员, Loki Entertainment Software
Lauren MacDonell, 首席程序员, Loki Entertainment Software

2000 年 2 月 01 日

Simple DirectMedia Layer (SDL) 的作者 Sam Lantinga 和 Lauren MacDonell 发布了这个系列的第一篇文章,在这些文章中,他们记录了从头设计和构建 Linux 游戏的过程。在本文中,作者说明了他们如何开始游戏的构思过程。

一切都是在某个星期天下午开始的,当时我们沿着州际 5 号公路从 Stockton 驾车回家。就在 Santa Nella 的南边,我们开始讨论编写我们自己的计算机游戏的可行性。由于有人请我们撰写关于在游戏开发中使用 SDL 的系列文章,我们才有了这个想法,而这是其中的第一篇。由于有一些游戏开发的实际经验,我们认为要熟悉和了解某个过程的唯一方法是亲身体验它,并且用文档记录下各个阶段。接着就是总结我们已经采取的预备步骤,以及游戏开发至今为止我们得到的建议。

构思游戏

6 小时的路程给了我们足够的时间开始激发脑力。首先,我们考虑要编写哪种游戏。街霸?冒险游戏?策略游戏?我们参与角色扮演游戏的历史很长,并且成绩出色,因此我们认为开发冒险游戏应该很有趣。

那么就引出了下一个问题:哪种类型?是科幻冒险?中世纪幻想?探宝历险?提到“探宝历险”时,突然冒出了一些灵感的火花。我们开始想像那种震撼的感觉:大把的金币从指尖滑落……,风帆在头顶拍打,空气中弥漫着盐和海水的味道……,恐惧地看着一个人走在甲板上,独脚海盗船长向远处眺望……。几分钟之后,我们已陷入了沉思中,毫无疑问我们的方向已定。

我们讨论了激烈的舰对舰战斗的各种情形,而那种世界下角色扮演游戏的可行性引起了我们的兴趣。我们边驾驶边讨论了一些游戏的具体细节。我们需要一个全面计划。一个经典计划,并且它要集角色扮演和战术战斗于一身,那就是寻宝。游戏中有几位海盗船长,而玩家将扮演其中一位,每个海盗船长都有一部分寻宝图,而他必须想办法收集其余部分的地图并得到宝藏。


Pirates Ho! 地图草稿
Pirates Ho! 地图草稿  

到家之前,假设已经变更了好几次。最初,宝藏是最终目标,但如果宝藏可以带领玩家去寻找新的或更刺激的财宝,那又会如何呢?或许,它是神秘的炼金术配方,可以使铅变成金子?或者是通向另一个空间的门户?可能的情况是无穷无尽的,所以我们决定研究所有海盗电影、书籍,特别是可以弄到手的游戏。因为我们希望获得一些历史和文学方面的观点,以及现有游戏中可能与我们相似的构思。




 

研究

到家之后,我们做的第一件事就是访问 CompUSA 来寻找游戏。我们找到几个关于海盗的游戏,它们自称包括了战斗、策略和角色扮演成分。我们有一点失望,因为游戏公司似乎事先窃取了我们的灵感,但我们很快就恢复了信心。因为在从 Stockton 回家的途中,我们就进行了激烈的争辩,是使游戏世界符合现实和历史,还是构造一个虚幻的世界,它包括美人鱼、海妖、巨大的旋涡以及类似的东西。我们所看到的正在销售的游戏似乎是符合现实/历史的,所以我们决定走相反的路,使我们的游戏尽可能有幻想。

在购买了一个我们找到的游戏后,我们把它拿回家,只是希望发现它并不如看上去那么好玩。这个海战游戏并没有很好地开发,也没有涉及到角色扮演的成分。很快,我们便认定我们可以使某些部分变得更好。我们搜索了 Internet,以查找其它海盗游戏的演示版及其评论,用它们来帮助确定我们游戏的范围。了解了其它游戏在评论家眼中的亮点和败笔,我们就对当前游戏尚未满足的要求有了一些认识。

我们租借了可以在当地录像带商店中找到的每一部海盗电影来继续进行研究。它们包括 Treasure Island、 Cutthroat Island、 Kidnapped,当然还有现代经典片 Muppet Treasure Island。根据这些影片,我们可以找到海盗电影的一些感觉:故事情节、场景、以及现代观众心目中的海盗形象和声音。这些是我们的游戏要考虑的因素。




 

越来越快乐!

我们在一个素不相识的人家中举行的意大利聚餐会上,惊奇地发现了游戏开发迄今为止最重要的一件事。我们应朋友之邀,而他又是个游戏程序员,所以我们就带上草稿和草图,希望他能花几分钟时间提供给我们一些新的想法。没想到主人和大多数宾客都是游戏程序员、QA 测试人员或热情的玩家,他们都有一些想法可以与我们分享。

我们一开始比较犹豫地提出了我们原来的构思,并解释了我们的想法。但却得到了热情的回应。每个人都有最中意的游戏愿意与我们分享,他们的想法对我们的计划是很好的补充,并且其中也不乏一些建设性的建议。现在,最关注的是游戏应该充满情调。一些使游戏增加情趣的建议包括添加一些随机的有趣事件,如在格斗期间添加一些船员的说明,以及在游戏中不同人物之间建立一组关系。

由于这是一个角色扮演的游戏,定制人物和船员相当重要;人们希望可以使他们的人物个性化,并且可以与船员中的人物交流。每个人都喜欢逻辑问题的构思,并建议可以有多种方法获胜。他们还希望有旁门左道的方法可以玩游戏,并且可以帮助完善人物。

当我们征求游戏的技术方面的反馈时,我们学到了最有价值的一课:在开始操心游戏的内部工作之前,先构建想法和故事。我们的新朋友告诉我们,不管代码如何巧妙复杂,一个不变的事实就是人们喜欢好的故事情节。




 

工具

我们在设计游戏世界和玩家界面时所遇到的第一个问题是“它看上去像什么?”。游戏开发的第一阶段就是概念艺术图;有一天晚上,我们很晚未眠,草拟出人物和环境的形象。我们从一开始就认定三维环境要比二维好,并且知道要有逼真的地图以及寻宝图。还需要想办法生成人物的图片,通过改照片或者徒手绘制。一旦有了关于外观的灵感,就必须找到适当的工具。幸好,我们家中已经有了 Bryce3D 和 Fractal Design Painter4 的副本,这样就可以在家中进行创作了。


构建游戏时作者的桌面
Pirates Ho! 地图草稿  

我们开始研究已经拥有的软件能力,并且在大量讨论和预算分析后,最终购买了包括 Bryce4、Poser4 和 Canoma 在内的软件包(请参阅本文后面的 参考资料 )。目前,已经成功创建了非常好的 18 世纪风格的游戏世界地图,以及一些三维岛屿,但仍必须想出处理小镇、船只、人物以及其它一些可视元素的算法。我们仍在讨论是否要投资买一部数字像机。




 

结束语

迄今为止,我们认为已经开了个好头。当然,希望在实现了我们的想法以后有些事可以改变 -- 毕竟,事物很少按人们的意志发展。

在开发过程的最初部分,我们学到了一些东西,并希望它们会有所帮助。首先,不要指望某个下午坐在电脑前,就可以写游戏。这也许适合某些人,但我们大多数人必须 -- 或至少应该 -- 首先创建好的构思。开始时,尽可能多地创建一些构思。如果可以的话,听听别人对您的构思有什么看法 -- 越多越好!

一旦确定了一些构思,尽量多做一些调查,看看市场上是否有同类产品,以及还有哪些需要没有满足。考虑一下,如果您是这个游戏玩家,那么需要什么,有什么期望;是要将游戏卖给别人,还是只想打发空闲时间,搞清楚这一点非常重要。同样,还要查看一些能从中获取灵感的书籍、电影、游戏等。当然,我们不是暗示您去剽窃什么,但从其它资料中汲取灵感并没有错。此外,彻底探索要工作的领域,就可以了解到玩家希望什么样的行为模式、人物、故事情节和场景,以及有什么创新能够带给他们惊喜。也许,还可能发现这个想法已经实现了,那么就可以在创建游戏的新构思时享受别人的劳动成果了。

要选择正确的工具,应考虑一些事情。首先,应根据要创建的事物选择工具,决不能反着干。换句话说,不要跑出去买回一大堆软件,结果让它们永远躺在书架上。想清楚要做什么,然后到处找一找适合这项工作的东西。其次,我们认为不是每个人都舍得花钱买那些我们刚好能负担得起的好软件。我们不会推荐特定软件包,但可以介绍几个好的 Internet 站点(请参 参考资料),可以在这些站点上找到各种共享软件和低成本应用程序。另一种选择就是在决定购买某个公司的整个软件包之前,先尝试下载和研究该软件的演示版本。

我们仍在学习中,希望您可以从我们的成果中学到一些东西,但这决不是开发游戏的唯一方法。我们认为这个游戏仍有值得改进的地方。只要知道如何实现目标,我们并不介意对游戏做改动。在探索过程中,您将会找到自己的方法。如果发现确实有值得借鉴的地方,我们也乐于听到。

祝好运!


SDL用法,第2部分:"PiratesHo!"编码


摘自:ChinaUnix文档频道 作者:HonestQiao 编辑:周荣茂 
级别: 初级

Sam Lantinga, 首席程序员, Loki Entertainment Software

2000 年 3 月 01 日

上个月,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 个步骤:

  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
    ])
    

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

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

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

  5. 自举配置进程:automake 需要复制一些辅助文件,它使用这些文件来检测当前的体系结构等情况。要自举 automake,重新运行 "automake -a -v -c --foreign" 直到它不再告诉您它正在复制文件为止。
  6. 创建 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; istatus() != 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 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 
#include 
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 谱写了样本主题音乐。以下就是结果:


样本闪屏 
样本闪屏  

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




 

结束语

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


SDL用法,第3部分:图形设计


摘自:ChinaUnix文档频道 作者:HonestQiao 编辑:周荣茂 
级别: 初级

Sam LantingaLoki Entertainment Software
Lauren MacDonellLoki Entertainment Software

2000 年 4 月 01 日

Sam Lantinga 和 Lauren MacDonell 目前正在为 ”Pirates Ho!” 开发故事和进行图形设计,这是他们为 Linux 开发的原创探宝历险角色扮演游戏。在日记的这一部分中,作者讲述了故事开发过程以及他们的灵感源泉。他们还详细说明了如何设计游戏中使用的图像。

好的游戏由故事和画面组成。我们相信某些游戏需要比其它游戏更好的故事。在构思故事时,我们依赖于特殊的开发规则和我们的想像力。而画面同样是技术和创造性过程的结合。

好故事的重要性

某些游戏玩家说好的情节构成了一个好游戏。而另一些说复杂和逼真的图像使游戏玩起来更过瘾。在 "Pirates Ho!" 中,游戏的决定性因素是故事。

为什么要在故事上投入这么多精力?首先,"Pirates Ho!" 是一个冒险游戏,因此游戏中的打斗场面都是围绕着故事情节展开的。如果我们开发的是街霸类游戏,就不必将情节限制到与坏人作战和收集财宝。但构建游戏需要的角色扮演成分需要扣人心弦的故事情节作为人物开发和交互的基础。

根据以往的经验,我们还发现好的打斗和策略游戏有很多,而好的角色扮演游戏却很少。考虑到构思好的故事所要投入的时间、精力和创造力,这倒也合理。但这对于那些除打斗和策略之外还喜欢好的故事的玩家而言却是不幸的。所以,我们决定在最大可能范围内开发所有这些成分。游戏中不仅包括了惊心动魄的打斗和具有挑战性的战略成分,还讲述了一个完整的故事,我们希望玩家能从中丰富其经验。

最后,我们希望 "Pirates Ho!" 能引起广泛的注意。我们有许多热心的读者和影迷朋友,他们并不是游戏迷,但他们说过他们也会因玩过一个很棒的冒险游戏而感到兴奋并由此而产生兴趣。我们希望结合了策略、打斗和角色扮演的 "Pirates Ho!" 能够吸引铁杆游戏迷和非游戏迷。




 

故事开发过程

在某些情况下,故事开发纯粹是一个创造性过程,而不是技术过程。然而,在 "Pirates Ho!" 中,我们在开发故事时却面临了各种各样的技术问题。

编程技术就引起了一个技术问题。我们不能为玩家设计出游戏引擎不能支持的场景。例如,我们不能将大规模海战直接写到游戏中,因为我们开发的战术战斗系统同时只能处理几艘船之间现实和具体的战斗。因此,它并不适合于开发涉及大规模政治冲突和其它全球性事件的主要情节。幸运的是,光是个别人物之间的关系和小规模战争就已经足够开发出一个好故事了。

我们开发故事也是一个技术过程,就是说,我们系统地和慎重地选择要使用的元素。除了包括所有我们认为有趣和好玩的老一套之外,我们还尝试创建适当的元素组合以促进人物开发和交互,这样才能跟上游戏的发展,使玩家关注下一步会发生什么。我们尝试创建一种故事情节,它能给予玩家很多机会来影响游戏环境并使玩家能方便地开发和改进人物。

构成虚构游戏世界的细节是故事开发的主要部分。我们拟定了游戏的社会和文化背景,以便给我们一个开发主要情节的环境。我们赞成这个世界中应该包括不可思议的事、地方和生物,以及具有魔力的人。但我们还没有确定魔法的范围有多广,以及谁具有魔力。有了魔法就使故事情节的发展变得更为扑朔迷离,使人物具有更广范围的动作。




 

灵感的源泉

在构思故事时,我们从各种原始资料中汲取灵感。当然,我们开始研究的还是经典海盗故事,如 金银岛 (Treasure Island)。这些故事使我们了解这类故事的一般规律:

  1. 好人(相对来说)发现或继承了一张地图,或者接收到指令要去见某人,而这个人最终会给他一张地图。
  2. 好人与坏人发生冲突,坏人想要得到地图并且/或者干掉好人。
  3. 坏人抓住了好人并且/或者从他身边夺走地图。
  4. 好人不得不逃亡并且/或者从坏人那里抢回地图。
  5. 好人找到财宝,杀死或打伤了坏人。

我们的故事符合基本规律,但各个事件可能不是按以上列出的顺序发生,并且每件事可能发生好几次。可选的故事元素包括爱情小插曲(必定有人受到了坏人的绑架),各种好人的联盟,增添喜剧效果的蠢人。我们包括了至少一个可选元素,但您必须等待以找出是哪些。

由于我们的游戏是在一个虚幻世界中,我们还希望结合一些在西方文学作品中不常与海盗联系在一起的元素。在故事中添加异国情调的成分使我们感到游戏更有趣味,我们希望这对玩家也能产生同样的效果。例如,大多数地名都引用了古波斯语。我们从许多故事借鉴了构思,如“天方夜谭”、Terry Pratchett 的 "Discworld" 系列和“南方公园”。




 

画面

概念艺术画 
在设计游戏的图像时,我们首先在纸上画出构思的轮廓。我们的概念艺术画从素描和人物线条画开始,经过了各个阶段的发展,使用了各种介质,逐渐变成了游戏中使用的图像。这个过程非常重要,因为它可以让我们使用不同的视觉元素,并确定实现我们所期望的视觉效果的最佳方法。将构思画在纸上给朋友们看可以帮助我们得到反馈意见。这里有一些 "Pirates Ho!" 概念艺术画的样本:


对话界面 
 

小镇界面 
 

地图界面 
 



 

图形设计

我们使用 Bryce4、Fractal Design Painter 4 和 GIMP 完成了 "Pirates Ho!" 的一些初始画面。以下是我们创建图像时使用的一些技术的简要描述。

舰船原型 
要构建舰船的模型,我们组合了 Bryce 中的基本几何图形。使用布尔运算符来结合使用正和负的形式,我们创建了舰船的所有基本形状。对于舰船的身体,我们生成了一个扁球体,并长方体覆盖其上半部分。在将球体的值设置为正,将长方体的值设置为负,然后将它们并为一组之后,只看得出球体的底部、具有船只形状那一半。我们添加了三根又高又细的圆柱体作为桅杆,然后创建了帆。对于帆,我们组合了一个正的扁圆柱体和一个与之略有错位的负的相似扁圆柱体。这就意味着正的圆柱体正面的边缘部分是可见的,看上去或多或少像鼓起的帆。下一个挑战是构建使舰船有些特征的细节部分,如栏杆、大炮和其它小玩意。

布尔图像生成

布尔图像生成是一种图象生成过程,在这种方法中,“正”对象占据的空间被“负”对象扣除,生成一个被挖掉一部分的对象,或者两个或多个对象组合成一个结合了所有起作用对象的质量和体积的合成对象(请参阅 参考资料中的 Bryce4 用户指南)。


 

岛屿 
我们使用 Bryce 中的地形编辑工具来创建岛屿。我们使用地形编辑器中的画笔功能徒手画出岛屿的外形。我们使用高地和侵蚀画笔功能创建了不同的地形效果。我们为岛屿使用了不同的材质,最后确定使用“地中海丘陵”,其特征是灌木丛生,到处是岩石;一些看上去像岩石的材质适合于一些更小的岛屿。下一个难题是在岛上添加村庄、小镇、码头和人类社会的标志。主要建筑风格最好用造型简单、粉刷的一两层建筑。这不仅是因为它们易于建模,而且它很像真的地中海村庄。


 

世界地图 
我们使用 Painter 4 创建地图。在研制地图之前,我们花了很长时间观察实际图片和十六和十七世纪地图的复制品。游戏世界地图的所有元素都以我们在研究时发现的特征为基础。我们徒手绘制出岛屿的轮廓,然后不断调整它们,直到我们对颜色和形状感到满意。在将岛屿放到地图上时,我们尝试产生一种适合于旅行和经商的最佳排列,并且使这片区域的地理位置看上去不像是精心规划过的。最后,我们对地图应用纸张材质,并不断编辑它,直至它看上去非常古老。


 

人物显示屏幕 
我们还使用 Painter 创建了人物显示屏幕的原型,这个屏幕将在游戏开始时使用,以显示人物的统计数据和其它信息。为创建此屏幕的背景,我们复制了部分包含木板纹理的图形。我们略微调整了颜色和纹理,并使用经过修改的纹理创建看上去象船体特写的木板。

我们通过导入 Sam 穿着海盗服装的数字照片来创建人物肖像,将它填到大小合适的字段中,并使用 Painter 中的纹理工具应用纸张颗粒以及画笔来改变照片的效果。我们添加了框架,因为屏幕看上去有一点平,而肖像位于没有框架的背景上看起来不舒服。为创建框架,我们采用描金木制框架的图片,逐段复制并粘贴它,直到它大小合适。我们添加了卷轴以突出显示人物的姓名,并使它与屏幕上的其它信息分开。我们通过下载一个卷轴图像来创建它,并更改它的颜色和形状以适合我们的显示屏幕。

然后添加一个值与背景大致相同的半透明字段来显示人物统计数据。我们让它部分透明,因为我们想要显示出木板;那样屏幕的左边的详细信息稍多一点,这就与肖像保持了平衡。为形成对比,我们添加了作为人物统计数据和历史记录的白色文本字符(人物统计数据和角色扮演系统的其它元素会在将来的文章中讨论)。


 



 

游戏菜单屏幕

对于主菜单,我们想要有一种类似于发光屏幕的明亮虚幻的效果。我们创建一个海景作为背景,并在工具箱中查找合适的工具制作按钮。我们最终使用 GIMP 创建按钮,GIMP 有许多徽标插件和带免费字体的文本效果。我们不断尝试,直到找到接近我们要求的一种字体和效果。我们决定使用 "Glowing Hot" 和 18 号 Dragonwick 字体。

我们发现 GIMP 生成带层次的徽标,通过操纵这些层次可以对普通徽标输出执行很有趣的操作。我们消除了原来创建框按钮的想法,我们除去了背景层以直接在菜单图像的文本边缘获得一个鲜明效果。我们将图像保存为 PNG 格式图像,PNG 格式保存阿尔法通道,它可以由 SDL_image 库直接读取并以位块方式传送到屏幕。在测试过程中,我们将 SDL 阿尔法通道以位块方式传送的速度提高了将近 100%,并将这些优化放进了 SDL 1.1.3 CVS 代码库中。

我们还发现,通过除去不包括有鲜明效果的那层以外的所有 "Glowing Hot" 效果层,得到了比原来更明亮的按钮文本。我们在主菜单中使用了这种技术,当鼠标移到菜单选项上时,菜单选项就会“变亮”。


 



 

游戏的外观

目前,我们已经尝试完成了这样一个外观,它既适合游戏世界的技术层,又或多或少酷似 17 世纪的欧洲。例如,我们创建了符合当时艺术风格的世界地图和人物肖像。至于陆地和海景,我们使用明亮的颜色唤起对热带风光的联想,因为游戏世界中的天气有点像加勒比海气候。当然,我们期望游戏的外观会随着我们添加新元素以及不断改善使用设计工具的技巧而变得更为漂亮。




 

展望未来

我们刚准备好开始认真进行图形界面工作,我们将在下一篇文章中详细讨论这项工作。游戏中出现的动画片段也正在计划中。下一篇文章还将讨论脚本语言和使用 Lex 和 Yacc 构建用户界面。


SDL用法,第4部分:lex和yacc


Sam Lantinga, 首席程序员, Loki Entertainment Software

2000 年 5 月 01 日

在这部分中,我们将讨论所有 Linux 程序员工具库中的两种实用工具:lex 和 yacc。这些工具让我们轻松地构建了在我们基于 SDL 的 Linux 游戏 Pirates Ho! 中使用的脚本语言和 GUI 框架。

在设计 Pirates Ho! 时,我们需要一种简便的方法向玩家描述界面和对话框选项。我们需要简单、一致且灵活的语言来进行描述,因此我们寻找可以帮助我们构建脚本语言的工具。

谁想要另一段 bison?


我还在学校时,就已经对 "yacc" 这个词充满恐惧。它让我想到那些头发凌乱、面色苍白的学生低声念叨着编译器和符号表。所以我非常小心,尽量避免使用编译器类。但在开发游戏时,我鼓起勇气使用 yacc,希望它可以使编写脚本变得容易些。最后,yacc 不仅使编写脚本变得更容易,还使这个过程很有趣。




 

从基础开始


yacc 实际上非常易于使用。只要提供给它一组描述语法的规则,它就可以分析标记,并根据所见到的采取操作。对于我们使用的脚本语言,我们希望由浅入深,最初只是指定一些数字及其逻辑运算:

eval.y

%{
/* This first section contains C code which will be included in the output
   file.
*/
#include 
#include 
/* Since we are using C  , we need to specify the prototypes for some 
   internal yacc functions so that they can be found at link time.
*/
extern int yylex(void);
extern void yyerror(char *msg);
%}
/* This is a union of the different types of values that a token can
   take on.  In our case we'll just handle "numbers", which are of
   C int type.
*/
%union {
 int number;
}
/* These are untyped tokens which are recognized as part of the grammar */
%token AND OR EQUALS
/* Here we are, any NUMBER token is stored in the number member of the
   union above.
*/
%token  NUMBER
/* These rules all return a numeric value */
%type  expression
%type  logical_expression and or equals
%%
/* Our language consists either of a single statement or of a list of statements.
   Notice the recursivity of the rule, this allows us to have any
   number of statements in a statement list.
*/
statement_list: statement | statement_list statement
 ;
/* A statement is simply an expression.  When the parser sees an expression
   we print out its value for debugging purposes.  Later on we'll
   have more than just expressions in our statements.
*/
statement: expression
 { printf("Expression = %d\n", $1); }
 ;
/* An expression can be a number or a logical expression. */
expression: NUMBER
 |   logical_expression
 ;
/* We have a few different types of logical expressions */
logical_expression: and
 |           or
 |           equals
 ;
/* When the parser sees two expressions surrounded by parenthesis and
   connected by the AND token, it will actually perform a C logical
   expression and store the result into
   this statement.
*/
and: '(' expression AND expression ')'
 { if ( $2 && $4 ) { $$ = 1; } else { $$ = 0; } }
 ;
or: '(' expression OR expression ')'
 { if ( $2 || $4 ) { $$ = 1; } else { $$ = 0; } }
 ;
equals: '(' expression EQUALS expression ')'
 { if ( $2 == $4 ) { $$ = 1; } else { $$ = 0; } }
 ;
%%
/* This is a sample main() function that just parses standard input
   using our yacc grammar.  It allows us to feed sample scripts in
   and see if they are parsed correctly.
*/
int main(int argc, char *argv[])
{ yyparse();
}
/* This is an error function used by yacc, and must be defined */-
void yyerror(char *message)
{
 fprintf(stderr, "%s\n", message);
}
                    




 

如何提供输入?


既然我们已经有了一个可以识别标记序列的简单语法,将需要寻求一种将这些标记提供给语法分析器的方法。lex 这种工具可以接受输入,将它转换成标记,然后将这些标记传递给 yacc。下面,我们将描述 lex 要将其转换成标记的表达式:

eval.l

%{
/* Again, this is C code that is inserted into the beginning of the output */
#include 
#include "y.tab.h"  /* Include the token definitions generated by yacc */
%}
/* Prevent the need for linking with -lfl */
%option noyywrap
/* This next section is a set of regular expressions that describe input
   tokens that are passed back to yacc.  The tokens are defined in y.tab.h,
   which is generated by yacc.
 */
%%
\/\/.*  /* ignore comments */
-[0-9] |[0-9]  { yylval.number=atoi(yytext); return NUMBER; }
[ \t\n]  /* ignore whitespace */
&&  { return AND; }
\|\|  { return OR; }
==  { return EQUALS; }
.  return yytext[0];
%%
                    

现在,在当前目录中已经有了分析源码,我们需要一个 Makefile 来构建它们:

Makefile

all: eval 
y.tab.c: eval.y
 yacc -d $<
lex.yy.c: eval.l
 lex $<
eval: y.tab.o lex.yy.o
 $(CC) -o $@ $^
                    

缺省情况下,yacc 输出到 y.tab.c,lex 输出到 lex.yy.c,因此我们使用那些名称作为源文件。Makefile 包含了根据分析描述文件构建源码的规则。一切就绪之后,可以输入 "make" 来构建语法分析器。然后我们可以运行该语法分析器并输入脚本以检查逻辑。

expression: NUMBER | plus
 ;
plus: expression ' ' expression
 ;
                




 

将 lex 和 yacc 与 C 一起使用


对于将 lex 和 yacc 与 C 一起使用,有一些忠告。lex 和 yacc 输出到 C 文件,因此对于 C ,我们使用 GNU 等价物 flex 和 bison。这些工具可以让您指定输出文件的名称。我们还将通用规则添加到 Makefile,因此 GNU Make 会根据 lex 和 yacc 源码自动构建 C 源文件。这要求我们将 lex 和 yacc 源码分别重命名成 "lex_eval.l" 和 "yacc_eval.y",这样 Make 就会为它们生成不同的 C 源文件。还需要更改 lex 用于存储 yacc 标记定义的文件。bison 输出的头文件使用带 .h 后缀的输出文件名,而在我们这个示例中是 "yacc_eval.cpp.h"。以下就是新的 Makefile:


Makefile

all: eval 
%.cpp: %.y
 bison -d -o $@ $<
%.cpp: %.l
 flex -o$@ $<
yacc_eval.o: yacc_eval.cpp
lex_eval.o: lex_eval.cpp
eval: yacc_eval.o lex_eval.o
 $(CXX) -o $@ $^
                    




 

从字符串分析


缺省 lex 代码从标准输入读取其输入,但我们希望游戏能够分析内存中的字符串。使用 flex 很容易就能做到,只要重新定义 lex 源文件顶部的宏 YY_INPUT :

extern int eval_getinput(char *buf, int maxlen);
#undef YY_INPUT
#define YY_INPUT(buf, retval, maxlen) (retval = eval_getinput(buf, maxlen))
                    


我们将 eval_getinput() 的实际代码写入一个单独文件,使它变得非常灵活,这样它可以从文件指针或内存中的字符串中获取输入。为了使用实际代码,我们首先建立一个全局数据源变量,然后调用 yacc 函数 yyparse(),此函数会调用输入函数并对它进行分析。




 

使用多个语法分析器


我们希望在游戏中对脚本语言和 GUI 描述使用不同的语法分析器,因为它们使用不同的语法规则。这样做是可行的,但我们必须对 flex 和 bison 使用一些技巧。首先,需要将语法分析器的前缀由 "yy" 更改成独特的名称,以避免名称冲突。只要对 flex 和 bison 分别使用命令行选项就可以重命名语法分析器,对 flex 使用 -P ,对 bison 使用 -p 。然后,必须将代码中使用 "yy" 前缀的地方改成我们选择的前缀。这包括了对 lex 源码中 yylval 的引用,以及 yyerror() 的定义,因为我们将它放在了最后游戏的一个单独文件中。最终的 Makefile 如下所示:

Makefile 

all: eval 
YY_PREFIX = eval_
%.cpp: %.y
 bison -p$(YY_PREFIX) -d -o $@ $<
%.cpp: %.l
 flex -P$(YY_PREFIX) -o$@ $<
yacc_eval.o: yacc_eval.cpp
lex_eval.o: lex_eval.cpp
eval: yacc_eval.o lex_eval.o
 $(CXX) -o $@ $^
                    




 

脚本语言


我们从以上显示的代码(可以在 参考资料中找到下载的网址)着手,继续添加对函数、变量和简单流量控制的支持,最后得到了游戏的相当完整的解释型语言。以下就是一个可能的脚本样本:

example.txt 

function whitewash
{
        if ( $1 == "Blackbeard" ) {
                print("Pouring whitewash on Blackbeard!")
                if ( $rum >= 3 ) {
                        print("Pouring whitewash on Blackbeard!")
                        mood = "happy"
                } else {
                        print($1, "says Grr....")
                        mood = "angry"
                        print("Have some more rum?")
                          rum
                }
        }
}
pirate = "Blackbeard"
rum = 0
mood = "angry"
print($pirate, "is walking by...")
while ( $mood == "angry" ) {
        whitewash($pirate)
}
return "there was much rejoicing"
                    




 

使用 yacc 构建 GUI


我们将 GUI 构建成一组窗口小部件,它们都从基类继承属性。这就非常好地勾画出 yacc 分析其输入的方法。我们定义了一组对应于基类属性的规则,然后为每一个窗口小部件分别定义了规则,同样也定义了基类的规则。当语法分析器与窗口小部件的规则匹配时,我们可以放心地将窗口小部件指针转换成适当的类,并设置期望的属性。以下就是简单的按钮部件示例:

yacc_gui.y 

%{
#include 
#include 
#include "widget.h"
#include "widget_button.h"
#define PARSE_DEBUG(X) (printf X)
#define MAX_WIDGET_DEPTH 32
static int widget_depth = -1;
static Widget *widget_stack[MAX_WIDGET_DEPTH];
static Widget *widget;
static void StartWidget(Widget *the_widget)
{
 widget_stack[widget_depth  ] = widget = the_widget;
}
static void FinishWidget(void)
{
 Widget *child;
 --widget_depth;
 if ( widget_depth >= 0 ) {
  child = widget;
  widget = widget_stack[widget_depth];
  widget->AddChild(child);
 }
}
%}
[tokens and types skipped for brevity]
%%
widget: button
 { FinishWidget();
   PARSE_DEBUG(("Completed widget\n")); }
 ;
widget_attribute:
 widget_area
 ;
/* Widget area: x, y, width, height */
widget_area:
 AREA '{' number ',' number ',' number ',' number '}'
 { widget->SetArea($3, $5, $7, $9);
   PARSE_DEBUG(("Area: %dx%d at (%d,%d)\n", $7, $9, $3, $5)); }
 ;
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/* The button widget */
button:
 button_tag '{' button_attributes '}'
 { PARSE_DEBUG(("Completed button\n")); }
 ;
button_tag:
 BUTTON name
 { StartWidget(new WidgetButton($2));
   PARSE_DEBUG(("Starting a button: %s\n", $2));
   free($2); }
 ;
/* The button widget attributes */
button_attributes:
 button_attribute
| button_attributes button_attribute
 ;
button_attribute:
 widget
| widget_attribute
| button_normal_image
| button_hover_image
 ;
button_normal_image:
 IMAGE file
 { ((WidgetButton *)widget)->LoadNormalImage($2);
   PARSE_DEBUG(("Button normal image: %s\n", $2));
   free($2); }
 ;
button_hover_image:
 HOVERIMAGE file
 { ((WidgetButton *)widget)->LoadHoverImage($2);
   PARSE_DEBUG(("Button hover image: %s\n", $2));
   free($2); }
 ;
                    




 

GUI 示例


以下是我们的主菜单,以它作为使用这种技术构建的 GUI 的示例:

main_menu.gui 

background "main_menu" {
 image "main_menu"
 button "new_game" {
  area { 32, 80, 370, 64 }
  image "main_menu-new"
  hover_image "main_menu-new_hi"
  #onclick [ new_gui("new_game") ]
  onclick [ new_gui("character_screen") ]
 }
 button "load_game" {
  area { 32, 152, 370, 64 }
  image "main_menu-load"
  hover_image "main_menu-load_hi"
  onclick [ new_gui("load_game") ]
 }
 button "save_game" {
  area { 32, 224, 370, 64 }
  image "main_menu-save"
  hover_image "main_menu-save_hi"
  onclick [ new_gui("save_game") ]
 }
 button "preferences" {
  area { 32, 296, 370, 64 }
  image "main_menu-prefs"
  hover_image "main_menu-prefs_hi"
  onclick [ new_gui("preferences") ]
 }
 button "quit_game" {
  area { 32, 472, 370, 64 }
  image "main_menu-quit"
  hover_image "main_menu-quit_hi"
  onclick [ quit_game() ]
 }
}
                    

在这个屏幕描述中,窗口小部件和属性由语法分析器进行分析,按钮回调由脚本语法分析器解释。new_gui() 和 quit_game() 是导出到脚本机制的内部函数。


主菜单
主菜单屏幕快照  



 

结束语


在我们的脚本和 GUI 设计语言的设计过程中,lex 和 yacc 是非常重要的工具。它们起初似乎令人胆怯,但使用过一段时间后,您就会发现它们使用起来既方便有顺手。下个月请再度光临我们的专栏,届时我们将开始把它们组合在一起,并带领您进入 Pirates Ho!的世界。


SDL用法,第5部分:"PiratesHo!"中界面设计的原则

摘自:ChinaUnix文档频道 作者:HonestQiao 编辑:周荣茂 
级别: 初级

Sam Lantinga, 首席程序员, Loki Entertainment Software
Lauren MacDonell, 技术作家, Loki Entertainment Software

2000 年 8 月 01 日

Sam Lantinga 和 Lauren MacDonell 在开发 “Pirates Ho!” 游戏的用户界面时投入了大量精力。在日记(记录了这个冒险探宝和角色扮演游戏的创建过程)的这一部分中,作者描述了他们开发界面初步设计时所使用的原则。

什么是用户界面?

在设计用户界面时,首先要考虑定义它的用途。我们的三个主要目标是用户的交互、信息和娱乐。




 

交互

界面可以让玩家与游戏交互,无论游戏是由几行简单的文本还是 3-D 图形奇景组成。界面的首要用途是在游戏逻辑和玩家之间转换数据。没有界面,就不能玩游戏。界面告诉玩家在游戏中发生了什么事,让他或她据此作出响应。


两种用户界面设计(Angband 和雷神 III)
 



 

信息

界面显示了有关游戏中的环境、人物、对象和事件的信息。此信息可以让玩家作出决定。一般来说,一幅图片胜过任何语言的描述。对于界面而言,合并到游戏中的图像通常能够比文本更快且更有效地显示信息。通过图表或符号表示法就可以做到这一点(如某个人物还有多少条命)。


使用图形表示法的界面 (Raptor)
 



 

娱乐

如果界面看上去很有趣,那么娱乐能起到锦上添花的作用。我们喜欢那些有好的画面和迷人景色的游戏,因为视觉效果起到的作用甚至与最精彩的故事情节相同。此外,详细的插图可以让玩家很好地了解每个人物的图像或者环境的地方特色。如果没有插图,就会丢失这些虚构的细节。它们使游戏世界变得更丰富多彩。

这三个问题(交互、信息、娱乐)很大程度上决定了界面设计的方法。我们的任务很明确:必须创建一种使玩家与游戏交互的方法,它将以生动有趣的方式提供所有必需的信息。




 

什么是“好”的界面?

我们在开发计算机游戏、其它软件和特殊控制台游戏的用户界面方面有相当多的经验。因此我们非常清楚应该使用什么,不应该使用什么。我们还很幸运,交了一些在游戏开发界工作的朋友,他们的建议有助于我们加强和改善 的界面的定义。在开发 "Pirates Ho!" 的界面时,我们紧记的主要原则是:简易性、一致性和引人入胜。




 

简易性

在搜索“理想界面”的神奇规则时,最常听到的回答是简易性。换句话说,构建易于理解且不需要指导手册的界面。或者构建一个告诉玩家下一步该做什么的侧栏。

如果我们说界面是易于理解的,就表示无需一大堆中间步骤、命令或击键就可以访问和响应游戏中的信息。只要玩家需要,信息就会出现在屏幕上。他或她不必等很长时间并且费了好大劲才知道如何使用键盘、鼠标、游戏控制杆或其它工具来继续玩游戏。

Humongous Entertainment 的 "Pajama Sam 3: You Are What You Eat from Your Head to Your Feet." 是我们最喜欢的游戏之一。这个游戏的界面是经过精心设计的,因此将鼠标放置在一个对象上会改变箭头的外观。箭头的变化表示单击此对象会引发某些事。然后玩家可以单击此对象以推进游戏的情节发展。箭头改变成另一种样式,指出通向另一个屏幕的小路。将箭头移到屏幕底部,会弹出显示了库存项目的图片,此时玩家可以查看库存。在屏幕底部随库存一起弹出的是“计算机”图标,玩家可以使用该图标保存或退出游戏。必须承认,这个游戏适合于 3 到 8 岁的儿童。但界面的简易性可以使玩家沉浸于可爱有趣的故事情节中。故事的乐趣和这个游戏中的难题并没有被过于复杂的界面减少。这正是我们如此喜欢它的原因之一。


"Pajama Sam 3" 界面,包括库存
 

一些在游戏界工作的朋友提醒我们不要要求玩家阅读文字。他们告诉我们,玩家不会去阅读文字,除非必须这样做。他们只想玩游戏。这对我们很有帮助,因为迄今为止我们还没有创建打印手册的计划。但这还意味着我们必须很仔细,确保我们的界面非常简单、易于使用。




 

一致性

我们还发现一致性对于界面设计非常重要。我们偶尔会遇到看似很容易玩的游戏,但这只是在我们尝试切换到另一个画面和方式之前。但如果改变了画面或方式,就会应用不同的规则。最好在游戏各处以相同方式做相同的事。最好还能够始终在相同位置找到重要信息。我们对一致性的观点并不一定要求在整个游戏中都使用同样的屏幕布局。但我们建议在布局中使用的逻辑可以让玩家预感到可以在哪里找到信息,以及在游戏的不同部分中如何执行命令。

例如,我们最近一直在玩 Blizzard 的“暗黑破坏神 II”。我们特别注意界面一致性。游戏的一些基本命令可以从头至尾以相同方式使用。浏览各个菜单也非常容易,如:库存、买入/卖出/交易、技能、属性等。这些菜单都符合相同的布局,因此易于浏览。在游戏的任何阶段,玩家还可以将鼠标放在对象、人物和位置上,以查看极其有用的简要描述。


“暗黑破坏神 II”界面,包括库存菜单
 

“暗黑破坏神 II”在其视觉风格上也保持一致。画面、菜单和控制栏都有相同的中世纪幻想外观。这种视觉连续性并不是绝对必要的,但它对游戏经历添加了美学享受并增强了沉浸于游戏世界的幻觉。




 

引人入胜

我们大多数人有过完全被电影吸引的愉快经验。技术高超的电影摄制者会结合灯光和声音因素创建并维持逼真的虚幻世界,使我们在其中留恋忘返。最好我们的游戏也能如此具有吸引力。用户界面的各个元素有助于维持玩家直接参与游戏世界的幻想。图形元素可以让玩家在视觉上体验游戏世界的环境、活动和地方特色。音乐和声音效果创建了一种特殊情调,并使游戏的事件显得更栩栩如生。我们认为“雷神 III”就是此类极其逼真的游戏。如果没有令人毛骨悚然的声音效果和刺激的画面,它还会如此吸引人吗?


“雷神 III”界面
 

维持沉浸于游戏世界的幻想的另一种方法是将界面的元素伪装成游戏世界的一部分。影评家使用术语“情景”(diegetic) 来描述影片情节或环境中看得见的人或物所产生的声音(通常是音乐)。情景元素的一个简单示例就是伴随钢琴或投币式自动电唱机的出现而响起的音乐。音乐仍是电影配乐的一部分,但它已不是背景音乐,因为它已经成为影片的一部分了。就界面设计而言,没有比情景更好的词能够描述装饰成游戏一部分的工具、菜单或其它特性了。(此外,如果您在对话中使用它,您的朋友将对此留下深刻印象。)

让我们看一下游戏中情景元素的示例。Interplay 的 "Fallout II" 的界面包括了 "Pip Boy"(一种掌上电脑飞行器射击游戏,包含了探求物信息、电子地图显示和关于游戏世界的信息文件)。"Pip Boy" 还可以让玩家“等待”一段特定时间(指示游戏世界时间向前跳跃这段时间)。实际上,在游戏中 "Pip Boy" 用于从计算机的不同位置处检索信息。这个聪明的情景工具让玩家不必退出游戏世界就可以查找信息,从而使游戏世界和现实世界之间的界限变得更加模糊。


"Fallout II" 界面 - "Pip Boy" 画面
 



 

我们的界面怎么样?

那么,如果将它应用到 "Pirates Ho!" 界面会怎么样?我们仍在解决这个问题,但我们已经有了初步设计。这里要强调的词是初步。在完成设计之前,我们大概检查了全部内容,但那时似乎……




 

情节画面

我们界面的重点是情节画面。情节画面可以让玩家观察发生了什么事,以及获取关于地点、时间和人物的信息。这个画面显示战争、旅行和对话(换句话说,游戏的情节)。屏幕右边的大幅图片显示玩家所看到的实景或船只的俯视图,以及周围环境(在海上旅行和战争期间)。左上方的图片显示关于位置、环境、一天中的时间和人物当前所在位置的天气情况的信息。左下方的图片始终是一张地图,它可以是游戏世界的地图,也可能是一个小镇或更小区域的地图,这取决于人物旅行中所在的位置。

迄今为止,我们的计划是构建界面中的一切对象,以响应点击命令。以后可能会添加一些键盘快捷键或其它功能,但现在,那似乎是最简单也是最直接的方法。我们计划要包括对象、地点、按钮和人物的描述,只要玩家的鼠标放置在这些对象上,就会出现相应的描述。通过对于游戏中的所有交互都使用相同的布局,并提供随处可用的信息,希望能够平衡简易性、外观一致性和信息可用性。


游戏布局
 



 

航海日志

“航海日志”是我们设计中一个很好的剧情元素示例。我们使用它来装饰关于探求物状态、储备、库存和船员的信息。它类似于一本笔记本,有三个部分分别包含了不同的信息。在情节画面中,可以单击屏幕底部附近书本形状的图标来查看这些信息。一部分或一“页”日志将包含基本的探求物信息,它将被自动输入和更新。玩家可以在日志中输入他们自己的注释来详细说明此信息。下一页将显示关于储备和财产的详细信息,并且可以让玩家选择武器、储备和他们离船时随身携带的其它物品。第三页将显示关于船员的信息,并允许玩家将财产、武器和储备分配给船员。我们试图将日志中的摘要信息合并到情节画面中,这样玩家就不必费时间在不同的屏幕中搜索他们要找的东西。


航海日志
 



 

游戏世界信息

我们开发的另一个情景工具将记录游戏世界的历史、经济、地理和文化。此信息可以让玩家了解游戏世界中事件的基本模式。此信息将在由主制图师 Frederick Figgleworth 创建的参考工具“小岛简史”的环境中显示。Figgleworth 有时会在游戏中出现,他会与玩家交流。

依照前面反对让玩家阅读太多文字的声明,那么我们包括关于游戏世界的文字信息就有些自相矛盾了。那正是我们使界面这部分看上去符合游戏世界的原因之一。“简史”是我们认为概述游戏世界中操作模式的最简便方式。当然,玩家不必阅读历史记录。通过在游戏中交互和探险,他们最终会收集到相同的信息。“简史”旨在作为补充参考工具,而不是游戏世界信息的唯一来源。


“简史”参考工具
 



 

音频

尽管还没有确定范围,我们仍计划将音频特性合并到界面中。我们也许不会在整个游戏中都播放插曲,但可能会播放一些符合特殊场合气氛的音乐。我们已经聆听了一些以航海为主题的传统民歌和船歌,并请音乐家朋友帮助寻找一位小提琴家和一位六孔小笛演奏家。我们还希望在一些对话框画面中使用画外音,它可以以另一种方式告诉玩家有关游戏中遇到的人物的信息。当然,我们还会在战争场面中加入声音效果,并在人物交流和旅行场面中加入环境声音。




 

结束语

在进行了大量研究和讨论之后,现在我们有了用户界面的初步设计,随着 "Pirates Ho!" 开发工作的进行,我们将逐步完善这个设计。目前,我们非常满意界面设计的简易性和一致性,我们竭尽所能使用界面将玩家完全吸引到这个游戏世界中。


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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值