使用 Simple DirectMedia Layer 进行游戏编程

作者:Bob Pendleton

Simple DirectMedia Layer (SDL, www.libsdl.org) 是一个简单而强大的跨平台游戏和多媒体开发库。该库由 Sam Latinga 在 Loki Software, Inc. 工作期间开发,并用于他们的商业游戏项目。SDL 的开发旨在满足在多操作系统环境下工作的游戏开发者的需求,并用于 Linux 版本的 MaelstromHopkins FBICivilization: Call to PowerDescent 2MythII: SoulblighterRailroad Tycoon IITux Racer 以及更多游戏。SDL 网站列出了数百款使用 SDL 编写的游戏和应用程序。

SDL 官方支持 Linux、Windows、BeOS、Mac OS、Mac OS X、FreeBSD、OpenBSD、BSD/OS、Solaris 和 IRIX。SDL 也适用于 Windows CE、AmigaOS、Atari、QNX、NetBSD、AIX、Tru64 UNIX 和 SymbianOS。但是,这些操作系统尚未获得官方支持。这意味着如果您使用 SDL 编写应用程序,您可以将其以最小的改动移植到所有这些操作系统。SDL 提供了一种在当前使用的每个主要操作系统上编写游戏和多媒体应用程序的可移植方式。

安装 SDL

如果您使用的是最新版本的 Linux,您可能已经完整安装了 SDL。事实上,在我 Red Hat 8.0 系统上使用 ldd 快速检查 /usr/bin 后,发现了八个依赖于 SDL 的程序。

以下命令显示您的系统上是否安装了 SDL 库和 C/C++ 头文件

locate SDL.h
locate libSDL
locate sdl-config

如果所有这些命令都报告找到了文件,则很可能您已经完整安装了 SDL,您只需要确保它是最新的。sdl-config 程序检查 SDL 版本并获取 SDL 应用程序的编译和链接标志。如果找到了 sdl-config,请运行

sdl-config --version
以查看您拥有的 SDL 版本。如果 sdl-config 报告的版本低于 1.2.4,您应该安装更新的库。与大多数开源项目一样,SDL 也在不断开发中,因此如果您正在使用 SDL 进行开发,请定期检查新版本或加入 SDL 邮件列表之一以跟踪库更新。

如果未安装 SDL,您需要下载并安装它。您的发行版可能具有预编译的 SDL 软件包,因此您可以首先检查您的常用软件包来源。如果它是最新的,那么最简单的入门方法是从您的发行版安装 SDL 的 devel 或 dev 软件包。

本文中使用的源代码附带的 sdl-install.sh 文件是一个 shell 脚本,用于下载并安装 SDL 1.2.5 版及其所有附加库。该脚本必须以 root 身份在您想要 SDL 源代码的目录中运行。该脚本下载以下内容

如果您不使用 sdl-install.sh,请访问上面列出的网页,下载文件,解压缩它们,并按照相应的 README 文件中的说明安装库。通过运行以下命令测试您的新安装

sdl-config --version
如果它没有运行或给出的版本号低于您安装的版本,则安装失败。根据我的经验,当我不按照说明操作或在不同的位置留下了旧版本的 SDL 时,就会发生这种情况。如果 locate sdl-config 列出了多个位置,则删除旧的 SDL 安装(我不喜欢这样做),或者在旧版本上重新安装。sdl-install.sh 文件显示了如何使用 ./configure --prefix 将 SDL 安装到您想要的任何位置,但最安全和最简单的方法是安装到默认位置。

SDL 文档可以在 www.libsdl.org/docs.php 找到。在线文档位于 sdldoc.csn.ul.ie。支持库文档要么链接在其下载页面中,要么包含在源代码中,要么嵌入在 .h 文件中。示例程序包含在 SDL 中,其支持库是您自己项目的绝佳起点。

SDL 示例

文件 bounce.cpp [可在 ftp.linuxjournal.com/pub/lj/listings/issue110/6410.tgz 获取] 是一个使用 SDL 进行输入和图形,以及 SDL_ttf 加载 TrueType 字体编写的游戏。游戏本身有 1300 多行 C++ 代码,完整的软件包包括源代码、图像、TrueType 字体、makefile、sdl-install.sh 以及游戏中使用的字体和图像的许可证文件。找到您可以在游戏中合法使用的字体、图形和声音可能比编写游戏本身更费力。

要开始学习 SDL,请从 Linux Journal FTP 站点下载 Bounce 源代码,并使用 tar -xzvf bounce.tar.gz 解压缩它。然后运行 make 来构建程序。通过在命令行输入 bounce 来运行程序。您可以通过输入 bounce -fullscreen 在全屏模式下运行它。游戏的剧情是地球开始在太阳系中游荡,并有坠入太阳的危险。您的工作是通过用月球击打地球来使地球远离太阳。每次用月球击中地球,您都会获得一分,而每次地球击中太阳,游戏都会得分。该游戏旨在展示 SDL 的功能,而不是成为您见过的最有趣的游戏。

Game Programming with the Simple DirectMedia Layer

图 1. 游戏 Bounce

初始化 SDL

必须先初始化 SDL,然后才能使用任何 SDL 函数,方法是调用 SDL_Init()

if (-1 == SDL_Init((SDL_INIT_VIDEO |
                    SDL_INIT_TIMER |
                    SDL_INIT_EVENTTHREAD)))
{
  ...
}

SDL_Init() 的参数标识需要初始化的子系统。在这里,我告诉 SDL 初始化视频、定时器和子系统,并使用基于线程的事件处理。我也可以使用包罗万象的 SDL_INIT_EVERYTHING,但您应该只初始化程序使用的 SDL 部分。如果您不打算使用操纵杆或 CD-ROM,则没有理由初始化它们。您可以随时使用 SDL_InitSubSystem() 和 SDL_QuitSubSystem() 函数初始化和关闭子系统。

在程序关闭之前,务必通过调用 SDL_Quit() 关闭 SDL。SDL_Quit() 关闭所有 SDL 子系统,释放 SDL 使用的所有系统资源,并恢复视频模式。最好使用 atexit() 来确保 SDL_Quit() 在您的程序终止时运行。未能调用 SDL_Quit() 可能会使您的计算机处于奇怪的视频模式。

设置视频模式

选择视频模式时,请决定是在窗口中运行还是作为全屏应用程序运行。然后,选择窗口或屏幕的大小。如果您选择窗口,请决定用户是否可以调整其大小。然后,选择如何适应屏幕的颜色深度。在 Bounce 中,我使用了类似这样的代码

options = SDL_ANYFORMAT |  SDL_FULLSCREEN;
screen = SDL_SetVideoMode(640, 480,
                          0,
                          options);

前两个参数指定程序运行的屏幕或窗口的宽度和高度(以像素为单位)。要在全屏模式下使用特定的宽度和高度,您的 XF86Config-4(或某些 X 版本的 XF86Config)文件的屏幕部分必须列出指定的大小。如果 Bounce 无法在您的机器上以全屏模式运行,则很可能是因为您在 XF86Config-4 文件的屏幕部分中没有设置 640 × 480 模式。

第三个参数指定每像素位数。或者,如果设置为 0(零),则告诉 SDL 使用当前的显示深度。最好使游戏适应当前的显示深度,而不是指望将要运行代码的每台机器都支持您所需的像素格式。

最后一个参数允许您向 SDL 提供有关如何设置视频模式的详细说明。有近十几个选项可供选择。在 Bounce 中,我使用 SDL_ANYFORMAT 让 SDL 选择最佳可用模式。此选项强制您的代码适应您拥有的任何像素深度,但使用它可以提供更好的性能,但会增加一些额外的编码。SDL_FULLSCREEN 选项告诉 SDL 设置全屏模式。

SDL_SetVideoMode() 返回的值是指向 SDL_Surface 结构的指针。此结构非常详细地描述了屏幕。如果指针为 NULL,则表示您请求的视频模式不可用。但是,获得非 NULL 值并不意味着您获得了想要的一切。根据您指定的选项检查此结构的 flags 字段。我发现最好是少要求一些,并使用我收到的东西,这样我就避免了在我的代码中硬编码我的机器和操作系统的限制。

现在视频模式已配置,请使用 SDL_WM_SetCaption() 设置窗口标题和图标名称。这不是必需的;这是使程序更易于使用的一些小技巧

SDL_WM_SetCaption("Bounce", "Bounce")
加载资源

Bounce 启动之前,它必须加载和初始化它使用的资源。Bounce 必须初始化颜色,加载一些图像,并加载它用于绘制文本的字体。由于视频模式是使用 SDL_ANYFORMAT 设置的,因此所有这些资源都必须转换为匹配任意显示格式。以下代码以我们需要的格式创建红色像素

SDL_PixelFormat *pf = screen->format;
int red = SDL_MapRGB(pf, 0xff, 0x00, 0x00);

SDL_PixelFormat 结构是对屏幕像素的描述,SDL_MapRGB() 将标准的 24 位 RGB 颜色表示形式转换为像素值,该像素值在屏幕上绘制时显示该颜色。

加载图像稍微复杂一些

SDL_Surface *s0, *s1;
s0 = SDL_LoadBMP(name);
s1 = SDL_DisplayFormat(s0);
SDL_SetColorKey(s1,
                (SDL_SRCCOLORKEY |
                 SDL_RLEACCEL),
                black);
SDL_FreeSurface(s0);

核心 SDL 包括 SDL_LoadBMP(),它将 .bmp 格式的图像加载为 SDL_Surface。SDL_image 提供了用于加载许多其他图像格式的例程。图像的格式与创建时的格式相同。我们使用 SDL_DisplayFormat() 将其转换为显示格式。SDL_SetColorKey() 用于告诉 SDL,当它将此表面复制(blit)到另一个表面时,它应该忽略所有黑色像素。我这样做是为了当我将地球的图像复制到屏幕上时,不会复制任何黑色背景,并且只接触地球圆形形状内的像素。SDL_RLEACCEL 标志告诉 SDL 对图像进行行程长度编码 (RLE)。使用 RLE 编码的图像可以加快图像复制速度。

Bounce 使用一种 TrueType 字体,但有三种不同的尺寸、两种不同的颜色和三种不同的样式。使用 SDL_ttf 库,我编写了一个例程,该例程加载 TrueType 字体,将 0-127 范围内的每个 ASCII 字符渲染为 SDL_Surface,将每个字符转换为匹配屏幕,并保存每个字母的高度、宽度和前进量,以便我可以在屏幕上绘制字符串。

主循环

SDL 提供了一个基于事件的输入系统,非常类似于 X、Mac OS 和 Windows 使用的系统。当按下键或移动鼠标时,事件将被放入队列中。程序可以使用 SDL_WaitEvent() 等待事件,也可以使用 SDL_PollEvent() 轮询事件。主循环必须处理事件、更新游戏状态、绘制下一帧并重复直到完成。

等待还是轮询事件的决定会影响游戏的整体结构。我选择等待事件并使用心跳定时器来驱动动作。我喜欢这种组合,因为它使程序可以在事件发生时随时处理事件,同时控制 CPU 使用率。这两种品质在网络游戏中都很重要。

定时器使用以下代码初始化

timer = SDL_AddTimer(10, timerCallback, NULL);

这告诉 SDL 每十毫秒调用一个名为 timerCallBack 的例程。我的定时器回调使用 SDL_PushEvent() 发送事件。由于定时器回调在单独的线程中运行,因此即使游戏停止并等待事件,它们也可以发送事件。当它收到定时器事件时,Bounce 会检查是否是时候绘制另一帧了。定时器确保程序不会尝试绘制超过 100 帧/秒,同时允许游戏在必要时以较慢的速度运行。在我的机器上,它以 85 帧/秒的速度运行,这与我的显示器的刷新率相匹配。

Bounce 分为几个不同的页面。主循环处理所有页面通用的事件,例如在您按下 Esc 时退出程序或在您按下 F1 时暂停游戏。在主循环查看事件后,它将事件传递到当前页面。每个页面都是一个函数,它将 SDL_Event 作为其参数。每个页面都负责处理事件、跟踪时间和绘制屏幕。虽然这种方法会导致一些重复的代码,但它为程序员提供了更大的灵活性,并且它适合面向对象的设计,其中每个页面都是页面类的实例。以下示例显示了主循环的部分内容,并说明了事件如何传递到各个页面

while ((!done) && SDL_WaitEvent(&event))
{
  switch (event.type)
  {
  case SDL_QUIT:
    done = true;
    break;
  case SDL_KEYDOWN:
    switch(event.key.keysym.sym)
    {
    case SDLK_ESCAPE:
      done = true;
      break;
    case SDLK_F1:
      play = !play;
      break;
    }
    break;
  }
  if (play &&
      (!done) &&
      (NULL != currentPage))
  {
    currentPage(&event);
  }
}

全局变量 currentPage 指向当前页面的实现。当一个页面想要启动另一个页面时,它会初始化新页面并将指针设置为该页面。Bounce 有三个页面:程序启动时看到的欢迎页面,另一个页面处理游戏玩法,而“您赢了/您输了”消息是第三个页面。

欢迎页面中的事件处理程序如下所示

switch (e->type)
{
  case SDL_USEREVENT:
    switch (e->user.code)
    {
    case MY_TIMEREVENT:
      now = SDL_GetTicks();
      dt = now - lastTime;
      if (dt >= minFrameTime)
      {
        drawWelcome(dt);
        lastTime = now;
      }
      break;
    }
    break;
case SDL_MOUSEBUTTONDOWN:
  initBounce();
  currentPage = bounce;
  break;
}

当此代码看到定时器事件时,它会检查自上次更新屏幕以来已经过了多长时间,并调用 drawWelcome() 来动画显示屏幕。当它看到鼠标按钮已被按下时,它会通过调用 initBounce() 使其准备就绪,然后将 currentPage 设置为指向游戏页面来切换到游戏页面。下一次通过时,将调用主循环 bounce()。

动画

动画例程使用脏像素技术,因此每帧只重绘屏幕的一小部分。使用此技术,我们跟踪对象上次绘制的位置和新位置。当 Bounce 绘制地球时,首先它通过用背景颜色填充脏像素来擦除地球所在的位置,然后在新位置绘制地球。我们使用以下代码填充矩形并绘制图像

SDL_FillRect(screen, rectangle, color);
SDL_BlitSurface(image, NULL, screen, rectangle);

SDL_FillRect() 用颜色填充 SDL_Surface(如屏幕)中的矩形。矩形使用 SDL_Rect 结构指定,颜色使用 SDL_MapRGB() 创建。SDL_BlitSurface() 将一个表面中的矩形复制到另一个表面中的矩形。如果源矩形为 NULL,则复制整个表面。SDL_BlitSurface() 是应用颜色键并利用 RLE 编码的例程。

总结

SDL 减少了在 Linux 上编写游戏所需的时间。它足够小,学习它是一个项目,而不是一项职业,并且它对于商业应用程序来说足够强大。我希望通过本文中的信息和 Bounce 的源代码,您已经学到了足够的 SDL 知识,可以开始修改 Bounce 并构建您自己的 SDL 游戏。

资源

Game Programming with the Simple DirectMedia Layer
电子邮件:bob@pendleton.com

Bob Pendleton 的第一个编程任务是将游戏从 HP 小型计算机移植到 UNIVAC 大型机,从那时起,他就一直对计算机游戏着迷。自 1981 年以来,他一直从事各种版本的 UNIX 和 Linux 的工作。他是一名独立的软件开发人员和作家。您可以通过 Bob@Pendleton.com 与他联系。

加载 Disqus 评论